add crafting goal

This commit is contained in:
Rokas Puzonas 2025-01-03 17:35:41 +02:00
parent d6a141d098
commit df56eceab6
21 changed files with 844 additions and 930 deletions

View File

@ -18,7 +18,15 @@ pub const Item = @import("./schemas/item.zig");
pub const Status = @import("./schemas/status.zig");
pub const Position = @import("./schemas/position.zig");
pub const Map = @import("./schemas/map.zig");
// pub const Character = @import("./schemas/character.zig");
pub const Character = @import("./schemas/character.zig");
pub const Equipment = @import("./schemas/equipment.zig");
pub const Craft = @import("./schemas/craft.zig");
pub const Resource = @import("./schemas/resource.zig");
pub const MoveResult = @import("./schemas/move_result.zig");
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
pub const GatherResult = SkillUsageResult;
pub const CraftResult = SkillUsageResult;
// pub const ServerStatus = @import("./schemas/status.zig");
// pub const Map = @import("./schemas/map.zig");
// pub const Position = @import("position.zig");

View File

@ -2,10 +2,12 @@
const std = @import("std");
const Store = @import("../store.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const json_utils = @import("../json_utils.zig");
pub const Equipment = @import("./equipment.zig");
const Task = @import("./task.zig");
const SimpleItem = @import("./simple_item.zig");
const Position = @import("./position.zig");
const Character = @This();
@ -139,6 +141,8 @@ equipment: Equipment,
task: ?TaskMasterTask,
inventory_max_items: u64,
inventory: Inventory,
position: Position,
cooldown_expiration: ?f64,
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
const name = try json_utils.getStringRequired(obj, "name");
@ -171,6 +175,14 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
.air = try ElementalStats.parse(obj, "attack_air", "dmg_air", "res_air"),
});
const x = try json_utils.getIntegerRequired(obj, "x");
const y = try json_utils.getIntegerRequired(obj, "y");
var cooldown_expiration: ?f64 = null;
if (json_utils.getString(obj, "cooldown_expiration")) |date_time| {
cooldown_expiration = parseDateTime(date_time) orelse return error.FailedToParseCooldownExpiration;
}
return Character{
.name = try Name.fromSlice(name),
.account = try Account.fromSlice(account),
@ -181,7 +193,9 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Character {
.equipment = try Equipment.parse(store, obj),
.task = try TaskMasterTask.parse(store, obj),
.inventory_max_items = inventory_max_items,
.inventory = try Inventory.parse(store, inventory)
.inventory = try Inventory.parse(store, inventory),
.position = Position.init(x, y),
.cooldown_expiration = cooldown_expiration
};
}

74
api/schemas/cooldown.zig Normal file
View File

@ -0,0 +1,74 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const Cooldown = @This();
pub const Reason = enum {
movement,
fight,
crafting,
gathering,
buy_ge,
sell_ge,
cancel_ge,
delete_item,
deposit,
withdraw,
deposit_gold,
withdraw_gold,
equip,
unequip,
task,
christmas_exchange,
recycling,
rest,
use,
buy_bank_expansion,
const Utils = EnumStringUtils(Reason, .{
.{ "movement" , Reason.movement },
.{ "fight" , Reason.fight },
.{ "crafting" , Reason.crafting },
.{ "gathering" , Reason.gathering },
.{ "buy_ge" , Reason.buy_ge },
.{ "sell_ge" , Reason.sell_ge },
.{ "cancel_ge" , Reason.cancel_ge },
.{ "delete_item" , Reason.delete_item },
.{ "deposit" , Reason.deposit },
.{ "withdraw" , Reason.withdraw },
.{ "deposit_gold" , Reason.deposit_gold },
.{ "withdraw_gold" , Reason.withdraw_gold },
.{ "equip" , Reason.equip },
.{ "unequip" , Reason.unequip },
.{ "task" , Reason.task },
.{ "christmas_exchange", Reason.christmas_exchange },
.{ "recycling" , Reason.recycling },
.{ "rest" , Reason.rest },
.{ "use" , Reason.use },
.{ "buy_bank_expansion", Reason.buy_bank_expansion },
});
pub const fromString = Utils.fromString;
pub const toString = Utils.toString;
};
started_at: f64,
expiration: f64,
reason: Reason,
pub fn parse(store: *Store, obj: std.json.ObjectMap) !Cooldown {
_ = store;
const started_at = try json_utils.getStringRequired(obj, "started_at");
const expiration = try json_utils.getStringRequired(obj, "expiration");
const reason = try json_utils.getStringRequired(obj, "reason");
return Cooldown{
.started_at = parseDateTime(started_at) orelse return error.InvalidStartedAt,
.expiration = parseDateTime(expiration) orelse return error.InvalidExpiration,
.reason = Reason.fromString(reason) orelse return error.InvalidReason
};
}

View File

@ -3,11 +3,13 @@ const Store = @import("../store.zig");
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const SimpleItem = @import("./simple_item.zig");
const Character = @import("./character.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
const Craft = @This();
pub const Items = SimpleItem.BoundedArray(8);
pub const max_items = 8;
pub const Items = SimpleItem.BoundedArray(max_items);
pub const Skill = enum {
weaponcrafting,
@ -30,6 +32,18 @@ pub const Skill = enum {
pub const toString = Utils.toString;
pub const fromString = Utils.fromString;
pub fn toCharacterSkill(self: Skill) Character.Skill {
return switch (self) {
.weaponcrafting => Character.Skill.weaponcrafting,
.gearcrafting => Character.Skill.gearcrafting,
.jewelrycrafting => Character.Skill.jewelrycrafting,
.cooking => Character.Skill.cooking,
.woodcutting => Character.Skill.woodcutting,
.mining => Character.Skill.mining,
.alchemy => Character.Skill.alchemy,
};
}
};
skill: Skill,

View File

@ -106,3 +106,21 @@ pub fn parse(store: *Store, obj: std.json.ObjectMap) !Equipment {
.utility2 = try UtilitySlot.parse(store, obj, "utility2_slot", "utility2_slot_quantity"),
};
}
pub fn getSlot(self: Equipment, slot: Slot) ?Store.Id {
return switch (slot) {
.weapon => self.weapon,
.shield => self.shield,
.helmet => self.helmet,
.body_armor => self.body_armor,
.leg_armor => self.leg_armor,
.boots => self.boots,
.ring1 => self.ring1,
.ring2 => self.ring2,
.amulet => self.amulet,
.artifact1 => self.artifact1,
.artifact2 => self.artifact2,
.utility1 => if (self.utility1) |util| util.id else null,
.utility2 => if (self.utility2) |util| util.id else null,
};
}

43
api/schemas/ge_order.zig Normal file
View File

@ -0,0 +1,43 @@
// zig fmt: off
const std = @import("std");
const Character = @import("./character.zig");
const json_utils = @import("../json_utils.zig");
const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
const Store = @import("../store.zig");
const Item = @import("./item.zig");
const GEOrder = @This();
pub const max_id_size = 32;
pub const Id = std.BoundedArray(u8, max_id_size);
pub const Account = Character.Account;
pub const Code = Item.Code;
id: Id,
seller: Account,
item_id: Store.Id,
quantity: u64,
price: u64,
created_at: f64,
pub fn parse(store: *Store, obj: std.json.ObjectMap) !GEOrder {
const id = try json_utils.getStringRequired(obj, "id");
const seller = try json_utils.getStringRequired(obj, "seller");
const code = try json_utils.getStringRequired(obj, "code");
const quantity = try json_utils.getPositiveIntegerRequired(obj, "quantity");
const price = try json_utils.getPositiveIntegerRequired(obj, "price");
const created_at = try json_utils.getStringRequired(obj, "created_at");
return GEOrder{
.id = try Id.fromSlice(id),
.seller = try Account.fromSlice(seller),
.item_id = try store.items.getOrReserveId(code),
.quantity = quantity,
.price = price,
.created_at = parseDateTime(created_at) orelse return error.InvalidDataTime,
};
}
pub fn parseAndAppend(store: *Store, obj: std.json.ObjectMap) !Store.Id {
return try store.ge_orders.appendOrUpdate(try parse(store, obj));
}

View File

@ -0,0 +1,31 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const Character = @import("./character.zig");
const Cooldown = @import("./cooldown.zig");
const Map = @import("./map.zig");
const MoveResult = @This();
cooldown: Cooldown,
destination: Map,
character: Character,
fn parse(store: *Store, obj: std.json.ObjectMap) !MoveResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
const destination = json_utils.getObject(obj, "destination") orelse return error.MissingProperty;
return MoveResult{
.character = try Character.parse(store, character),
.destination = try Map.parse(store, destination),
.cooldown = try Cooldown.parse(store, cooldown)
};
}
pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !MoveResult {
const result = try parse(store, obj);
_ = try store.characters.appendOrUpdate(result.character);
return result;
}

View File

@ -20,6 +20,12 @@ pub fn subtract(self: Position, other: Position) Position {
return init(self.x - other.x, self.y - other.y);
}
pub fn distance(self: Position, other: Position) f32 {
const dx: f32 = @floatFromInt(self.x - other.x);
const dy: f32 = @floatFromInt(self.y - other.y);
return @sqrt(dx * dx + dy * dy);
}
pub fn format(
self: Position,
comptime fmt: []const u8,

View File

@ -1,6 +1,7 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const Character = @import("./character.zig");
const json_utils = @import("../json_utils.zig");
const DropRate = @import("./drop_rate.zig");
const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils;
@ -22,6 +23,15 @@ pub const Skill = enum {
pub const toString = Utils.toString;
pub const fromString = Utils.fromString;
pub fn toCharacterSkill(self: Skill) Character.Skill {
return switch (self) {
.mining => Character.Skill.mining,
.woodcutting => Character.Skill.woodcutting,
.fishing => Character.Skill.fishing,
.alchemy => Character.Skill.alchemy,
};
}
};
pub const Name = std.BoundedArray(u8, 32);
@ -29,7 +39,9 @@ pub const Name = std.BoundedArray(u8, 32);
pub const max_code_size = 32;
pub const Code = std.BoundedArray(u8, max_code_size);
pub const Drops = std.BoundedArray(DropRate, 16);
pub const max_drops = 16;
pub const Drop = DropRate;
pub const Drops = std.BoundedArray(DropRate, max_drops);
name: Name,
code: Code,

View File

@ -0,0 +1,23 @@
// zig fmt: off
const std = @import("std");
const json_utils = @import("../json_utils.zig");
const Resource = @import("./resource.zig");
const Store = @import("../store.zig");
const SimpleItem = @import("./simple_item.zig");
const SkillInfoDetails = @This();
pub const Items = SimpleItem.BoundedArray(Resource.max_drops);
xp: u64,
items: Items,
pub fn parse(store: *Store, obj: std.json.ObjectMap) !SkillInfoDetails {
const xp = try json_utils.getPositiveIntegerRequired(obj, "xp");
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return SkillInfoDetails{
.xp = xp,
.items = try Items.parse(store, items)
};
}

View File

@ -0,0 +1,31 @@
// zig fmt: off
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const Character = @import("./character.zig");
const Cooldown = @import("./cooldown.zig");
const SkillInfoDetails = @import("./skill_info_details.zig");
const SkillUsageResult = @This();
cooldown: Cooldown,
details: SkillInfoDetails,
character: Character,
fn parse(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
return SkillUsageResult{
.character = try Character.parse(store, character),
.cooldown = try Cooldown.parse(store, cooldown),
.details = try SkillInfoDetails.parse(store, details)
};
}
pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
const result = try parse(store, obj);
_ = try store.characters.appendOrUpdate(result.character);
return result;
}

View File

@ -22,6 +22,11 @@ const Monster = @import("./schemas/monster.zig");
const Resource = @import("./schemas/resource.zig");
const Position = @import("./schemas/position.zig");
const Map = @import("./schemas/map.zig");
const GEOrder = @import("./schemas/ge_order.zig");
const MoveResult = @import("./schemas/move_result.zig");
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
pub const GatherResult = SkillUsageResult;
pub const CraftResult = SkillUsageResult;
const Image = Store.Image;
const Server = @This();
@ -946,6 +951,101 @@ pub fn getImage(self: *Server, category: Image.Category, code: []const u8) Fetch
return image_id;
}
// https://api.artifactsmmo.com/docs/#/operations/get_ge_sell_order_grandexchange_orders__id__get
pub fn getGEOrder(self: *Server, id: []const u8) FetchError!?Store.Id {
const path_buff_size = comptime blk: {
var count = 0;
count += 22; // "/grandexchange/orders/"
count += GEOrder.max_id_size;
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/grandexchange/orders/{s}", .{ id }) catch return FetchError.InvalidPayload;
return try self.fetchOptionalObject(
FetchError,
null,
Store.Id,
GEOrder.parseAndAppend,
.{ .method = .GET, .path = path, .ratelimit = .data }
);
}
// https://api.artifactsmmo.com/docs/#/operations/action_move_my__name__action_move_post
pub fn move(self: *Server, character: []const u8, position: Position) errors.MoveError!MoveResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 12; // "/action/move"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/move", .{ character }) catch return FetchError.InvalidPayload;
var payload_buffer: [64]u8 = undefined;
const payload = std.fmt.bufPrint(&payload_buffer, "{{ \"x\":{}, \"y\":{} }}", .{ position.x, position.y }) catch return FetchError.InvalidPayload;
const result = try self.fetchObject(
errors.MoveError,
errors.parseMoveError,
MoveResult,
MoveResult.parseAndUpdate,
.{ .method = .POST, .path = path, .ratelimit = .actions, .payload = payload }
);
return result;
}
// https://api.artifactsmmo.com/docs/#/operations/action_gathering_my__name__action_gathering_post
pub fn gather(self: *Server, character: []const u8) errors.GatherError!GatherResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 17; // "/action/gathering"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/gathering", .{ character }) catch return FetchError.InvalidPayload;
return try self.fetchObject(
errors.GatherError,
errors.parseGatherError,
GatherResult,
GatherResult.parseAndUpdate,
.{ .method = .POST, .path = path, .ratelimit = .actions }
);
}
// https://api.artifactsmmo.com/docs/#/operations/action_crafting_my__name__action_crafting_post
pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u64) errors.CraftError!CraftResult {
const path_buff_size = comptime blk: {
var count = 0;
count += 4; // "/my/"
count += Character.max_name_size;
count += 16; // "/action/crafting"
break :blk count;
};
var path_buff: [path_buff_size]u8 = undefined;
const path = std.fmt.bufPrint(&path_buff, "/my/{s}/action/crafting", .{ character }) catch return FetchError.InvalidPayload;
var payload_buff: [256]u8 = undefined;
const payload = std.fmt.bufPrint(&payload_buff, "{{ \"code\":\"{s}\", \"quantity\":{} }}", .{ item, quantity }) catch return FetchError.InvalidPayload;
return try self.fetchObject(
errors.CraftError,
errors.parseCraftError,
CraftResult,
CraftResult.parseAndUpdate,
.{ .method = .POST, .path = path, .payload = payload, .ratelimit = .actions }
);
}
// https://api.artifactsmmo.com/docs/#/operations/generate_token_token_post
pub fn generateToken(self: *Server, username: []const u8, password: []const u8) !AuthToken {
const base64_encoder = std.base64.standard.Encoder;

View File

@ -2,11 +2,13 @@
const std = @import("std");
const s2s = @import("s2s");
const Item = @import("./schemas/item.zig");
const SimpleItem = @import("./schemas/simple_item.zig");
const Character = @import("./schemas/character.zig");
const Task = @import("./schemas/task.zig");
const Monster = @import("./schemas/monster.zig");
const Resource = @import("./schemas/resource.zig");
const Map = @import("./schemas/map.zig");
const GEOrder = @import("./schemas/ge_order.zig");
const Position = @import("./schemas/position.zig");
const Skin = Character.Skin;
@ -261,7 +263,7 @@ fn Repository(comptime Object: type, comptime name_field: []const u8) type {
if (id < self.objects.items.len) {
return switch (self.objects.items[id]) {
.object => |obj| @field(obj, name_field).slice(),
.reserve => |name| name.slice(),
.reserved => |name| name.slice(),
};
}
@ -305,7 +307,9 @@ const Characters = Repository(Character, "name");
const Tasks = Repository(Task, "code");
const Monsters = Repository(Monster, "code");
const Resources = Repository(Resource, "code");
const GEOrders = Repository(GEOrder, "id");
const Maps = std.ArrayListUnmanaged(Map);
const Bank = SimpleItem.BoundedArray(64);
items: Items,
characters: Characters,
@ -314,6 +318,8 @@ monsters: Monsters,
resources: Resources,
images: Images,
maps: Maps,
bank: Bank,
ge_orders: GEOrders,
pub fn init(allocator: std.mem.Allocator) !Store {
const max_items = 512;
@ -322,6 +328,7 @@ pub fn init(allocator: std.mem.Allocator) !Store {
const max_monsters = 64;
const max_resources = 32;
const max_maps = 512;
const max_ge_orders = 128;
var items = try Items.initCapacity(allocator, max_items);
errdefer items.deinit(allocator);
@ -351,6 +358,11 @@ pub fn init(allocator: std.mem.Allocator) !Store {
var maps = try Maps.initCapacity(allocator, max_maps);
errdefer maps.deinit(allocator);
var ge_orders = try GEOrders.initCapacity(allocator, max_ge_orders);
errdefer ge_orders.deinit(allocator);
const bank = Bank.init();
return Store{
.items = items,
.characters = characters,
@ -359,6 +371,8 @@ pub fn init(allocator: std.mem.Allocator) !Store {
.resources = resources,
.maps = maps,
.images = images,
.bank = bank,
.ge_orders = ge_orders
};
}
@ -370,6 +384,7 @@ pub fn deinit(self: *Store, allocator: std.mem.Allocator) void {
self.resources.deinit(allocator);
self.maps.deinit(allocator);
self.images.deinit(allocator);
self.ge_orders.deinit(allocator);
}
const SaveData = struct {

View File

@ -1,3 +1,4 @@
// zig fmt: off
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
@ -5,13 +6,12 @@ const Allocator = std.mem.Allocator;
const Artificer = @import("artificer");
const Api = @import("artifacts-api");
// zig fmt: off
pub const std_options = .{
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .api, .level = .info },
.{ .scope = .artificer, .level = .debug },
}
};
// zig fmt: on
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
const args = try std.process.argsAlloc(allocator);
@ -37,9 +37,6 @@ pub fn main() !void {
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
// var artificer = try Artificer.init(allocator, token);
// defer artificer.deinit();
var store = try Api.Store.init(allocator);
defer store.deinit(allocator);
@ -48,33 +45,37 @@ pub fn main() !void {
try server.setToken(token);
const resources = try server.getResources(allocator, .{});
resources.deinit();
// var artificer = try Artificer.init(allocator, token);
// defer artificer.deinit();
std.log.info("Prefetching server data", .{});
{
const status: Api.Status = try server.getStatus();
const cwd_path = try std.fs.cwd().realpathAlloc(allocator, ".");
defer allocator.free(cwd_path);
const file = try std.fs.cwd().createFile("api-store.bin", .{});
defer file.close();
try store.save(status.version.slice(), file.writer());
const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store.bin" });
defer allocator.free(cache_path);
// TODO: Don't prefetch images
try server.prefetchCached(allocator, cache_path);
}
// std.log.info("Prefetching server data", .{});
// try artificer.server.prefetchCached(cache_path);
const character_id = (try server.getCharacter("Blondie")).?;
// if (false) {
// std.log.info("Starting main loop", .{});
// while (true) {
// const waitUntil = artificer.nextStepAt();
// const duration = waitUntil - std.time.milliTimestamp();
// if (duration > 0) {
// std.time.sleep(@intCast(duration));
// }
//
// try artificer.step();
var artificer = try Artificer.init(allocator, &server, character_id);
defer artificer.deinit(allocator);
// _ = try artificer.appendGoal(Artificer.Goal{
// .gather = .{
// .item = store.items.getId("sap").?,
// .quantity = 1
// }
// }
// });
_ = try artificer.appendGoal(Artificer.Goal{
.craft = .{
.item = store.items.getId("copper").?,
.quantity = 3
}
});
std.log.info("Starting main loop", .{});
try artificer.runUntilGoalsComplete();
}

View File

@ -1,183 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Position = Api.Position;
const Server = Api.Server;
const assert = std.debug.assert;
pub const Action = union(enum) {
move: Position,
fight,
gather,
deposit_gold: u64,
deposit_item: Api.ItemQuantity,
withdraw_item: Api.ItemQuantity,
craft_item: Api.ItemQuantity,
accept_task,
pub fn perform(self: Action, api: *Server, name: []const u8) !ActionResult {
const log = std.log.default;
switch (self) {
.fight => {
log.debug("[{s}] attack", .{name});
return .{
.fight = api.actionFight(name)
};
},
.move => |pos| {
log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y});
return .{
.move = api.actionMove(name, pos.x, pos.y)
};
},
.deposit_gold => |quantity| {
log.debug("[{s}] deposit {} gold", .{name, quantity});
return .{
.deposit_gold = api.actionBankDepositGold(name, quantity)
};
},
.deposit_item => |item| {
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] deposit {s} (x{})", .{name, code, item.quantity});
return .{
.deposit_item = api.actionBankDepositItem(name, code, item.quantity)
};
},
.withdraw_item => |item| {
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] withdraw {s} (x{})", .{name, code, item.quantity});
return .{
.withdraw_item = api.actionBankWithdrawItem(name, code, item.quantity)
};
},
.gather => {
log.debug("[{s}] gather", .{name});
return .{
.gather = api.actionGather(name)
};
},
.craft_item => |item| {
const code = api.store.getCode(item.id) orelse return error.ItemNotFound;
log.debug("[{s}] craft {s} (x{})", .{name, code, item.quantity});
return .{
.craft_item = api.actionCraft(name, code, item.quantity)
};
},
.accept_task => {
log.debug("[{s}] accept task", .{name});
return .{
.accept_task = api.acceptTask(name)
};
}
}
}
};
pub const ErrorResponse = enum {
/// Something went wrong, and you probably can't reasonbly recover from it. Bail, bail!
abort,
/// You probably were trying to an action a bit too early, just try again a bit later.
retry,
/// Something in your logic went wrong, re-evaluate your state and do something different.
restart,
/// The error can be safe ignored, continue doing the next action that you wanted.
ignore
};
pub const ActionResult = union(enum) {
move: Api.MoveError!Server.MoveResult,
fight: Api.FightError!Server.FightResult,
gather: Api.GatherError!Server.GatherResult,
deposit_gold: Api.BankDepositGoldError!Server.GoldTransactionResult,
deposit_item: Api.BankDepositItemError!Server.ItemTransactionResult,
withdraw_item: Api.BankWithdrawItemError!Server.ItemTransactionResult,
craft_item: Api.CraftError!Server.CraftResult,
accept_task: Api.AcceptTaskError!Server.AcceptTaskResult,
const AnyError = Server.MoveError;
const Tag = @typeInfo(ActionResult).Union.tag_type.?;
fn fieldType(comptime kind: Tag) type {
const field_type = std.meta.fields(ActionResult)[@intFromEnum(kind)].type;
return @typeInfo(field_type).ErrorUnion.payload;
}
pub fn get(self: ActionResult, comptime kind: Tag) ?fieldType(kind) {
return switch (self) {
kind => |v| v catch null,
else => null
};
}
pub fn getError(self: ActionResult) !void {
switch (self) {
.fight => |result| {
_ = try result;
},
.move => |result| {
_ = try result;
},
.deposit_gold => |result| {
_ = try result;
},
.deposit_item => |result| {
_ = try result;
},
.withdraw_item => |result| {
_ = try result;
},
.gather => |result| {
_ = try result;
},
.craft_item => |result| {
_ = try result;
},
.accept_task => |result| {
_ = try result;
}
}
}
pub fn getErrorResponse(self: ActionResult) ?ErrorResponse {
self.getError() catch |err| switch (err) {
error.CharacterIsBusy,
error.CharacterInCooldown,
error.BankIsBusy => return ErrorResponse.retry,
error.CharacterAtDestination => return ErrorResponse.ignore,
error.MapNotFound,
error.CharacterIsFull,
error.MonsterNotFound,
error.NotEnoughSkill,
error.ResourceNotFound,
error.NotEnoughGold,
error.BankNotFound,
error.ItemNotFound,
error.NotEnoughItems,
error.RecipeNotFound,
error.AlreadyHasTask,
error.TaskMasterNotFound,
error.WorkshopNotFound => return ErrorResponse.restart,
error.CharacterNotFound,
error.ServerUnavailable,
error.RequestFailed,
error.ParseFailed,
error.OutOfMemory => return ErrorResponse.abort
};
return null;
}
};
comptime {
const ActionTag = @typeInfo(Action).Union.tag_type.?;
const ResultTag = @typeInfo(ActionResult).Union.tag_type.?;
assert(std.meta.fields(ActionTag).len == std.meta.fields(ResultTag).len);
}

View File

@ -1,104 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Server = Api.Server;
const Position = Api.Position;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const CharacterTask = @import("./task.zig").Task;
const QueuedAction = @import("./action.zig").Action;
const QueuedActionResult = @import("./action.zig").ActionResult;
const Brain = @This();
name: []const u8,
action_queue: std.ArrayList(QueuedAction),
task: ?CharacterTask = null,
paused_until: ?i64 = null, // ms
pub fn init(allocator: Allocator, name: []const u8) !Brain {
return Brain{
.name = try allocator.dupe(u8, name),
.action_queue = std.ArrayList(QueuedAction).init(allocator),
};
}
pub fn deinit(self: Brain) void {
const allocator = self.action_queue.allocator;
allocator.free(self.name);
self.action_queue.deinit();
}
pub fn performNextAction(self: *Brain, api: *Server) !void {
const log = std.log.default;
assert(self.action_queue.items.len > 0);
const retry_delay = 500; // 500ms
const next_action = self.action_queue.items[0];
const action_result = try next_action.perform(api, self.name);
if (action_result.getErrorResponse()) |error_response| {
switch (error_response) {
.retry => {
self.paused_until = std.time.milliTimestamp() + retry_delay;
log.warn("[{s}] retry action", .{self.name});
return;
},
.restart => {
log.warn("[{s}] clear action queue", .{self.name});
self.action_queue.clearAndFree();
return;
},
.abort => {
log.warn("[{s}] abort action {s}", .{ self.name, @tagName(next_action) });
try action_result.getError();
// The error above should always return
unreachable;
},
.ignore => { },
}
}
_ = self.action_queue.orderedRemove(0);
if (self.task) |*task| {
task.onActionCompleted(action_result);
}
}
pub fn step(self: *Brain, api: *Api.Server) !void {
if (self.paused_until) |paused_until| {
if (std.time.milliTimestamp() < paused_until) {
return;
}
self.paused_until = null;
}
if (self.action_queue.items.len > 0) return;
if (self.task) |task| {
if (task.isComplete()) {
// if (try brain.depositItemsToBank(&self.server)) {
// continue;
// }
self.task = null;
}
}
if (self.task) |task| {
try task.queueActions(api, self.name, &self.action_queue);
}
}
pub fn cooldown(self: *Brain, api: *Server) i64 {
const character = api.store.getCharacter(self.name).?;
const cooldown_expiration: i64 = @intFromFloat(character.cooldown_expiration * std.time.ms_per_s);
if (self.paused_until) |pause_until| {
return @max(cooldown_expiration, pause_until);
} else {
return cooldown_expiration;
}
}

67
lib/craft_goal.zig Normal file
View File

@ -0,0 +1,67 @@
// zig fmt: off
const Api = @import("artifacts-api");
const Artificer = @import("./root.zig");
const Goal = @This();
item: Api.Store.Id,
quantity: u64,
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void {
const store = artificer.server.store;
const character = store.characters.get(artificer.character).?;
if (self.quantity == 0) {
artificer.removeGoal(goal_id);
return;
}
const item = store.items.get(self.item).?;
const craft = item.craft.?;
const skill = craft.skill.toCharacterSkill();
if (character.skills.get(skill).level < craft.level) {
return error.SkillTooLow;
}
const craft_multiples: u64 = @intFromFloat(@ceil(
@as(f32, @floatFromInt(self.quantity)) /
@as(f32, @floatFromInt(craft.quantity))
));
for (craft.items.items.slice()) |craft_item| {
const inventory_item_quantity = character.inventory.getQuantity(craft_item.id);
if (inventory_item_quantity < craft_item.quantity * craft_multiples) {
return error.NotEnoughItems;
}
}
const workshop_position = artificer.findNearestWorkstation(craft.skill).?;
if (!workshop_position.eql(character.position)) {
artificer.queued_actions.appendAssumeCapacity(.{
.goal = goal_id,
.action = .{ .move = workshop_position }
});
return;
}
artificer.queued_actions.appendAssumeCapacity(.{
.goal = goal_id,
.action = .{ .craft = .{ .item = self.item, .quantity = self.quantity } }
});
}
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
_ = goal_id;
if (result == .craft) {
const craft_result = result.craft;
const craft_quantity = craft_result.details.items.getQuantity(self.item);
if (self.quantity > craft_quantity) {
self.quantity -= craft_quantity;
} else {
self.quantity = 0;
}
}
}

51
lib/gather_goal.zig Normal file
View File

@ -0,0 +1,51 @@
// zig fmt: off
const Api = @import("artifacts-api");
const Artificer = @import("./root.zig");
const Goal = @This();
item: Api.Store.Id,
quantity: u64,
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) void {
const store = artificer.server.store;
const character = store.characters.get(artificer.character).?;
if (self.quantity == 0) {
artificer.removeGoal(goal_id);
return;
}
const resource_id = artificer.findBestResourceWithItem(self.item) orelse @panic("Failed to find resource with item");
const map_position: Api.Position = artificer.findNearestMapWithResource(resource_id) orelse @panic("Map with resource not found");
if (!map_position.eql(character.position)) {
artificer.queued_actions.appendAssumeCapacity(.{
.goal = goal_id,
.action = .{ .move = map_position }
});
return;
}
artificer.queued_actions.appendAssumeCapacity(.{
.goal = goal_id,
.action = .{ .gather = {} }
});
}
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
_ = goal_id;
if (result == .gather) {
const gather_result = result.gather;
const gather_quantity = gather_result.details.items.getQuantity(self.item);
if (self.quantity > gather_quantity) {
self.quantity -= gather_quantity;
} else {
self.quantity = 0;
}
}
}

View File

@ -3,134 +3,326 @@ const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
pub const Brain = @import("./brain.zig");
pub const TaskGraph = @import("./task_graph.zig");
const GatherGoal = @import("gather_goal.zig");
const CraftGoal = @import("craft_goal.zig");
const assert = std.debug.assert;
const log = std.log.scoped(.artificer);
const Artificer = @This();
pub const GoalId = packed struct {
const Generation = u5;
const Index = u11;
generation: Generation,
index: Index
};
const max_goals = std.math.maxInt(GoalId.Index);
const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms
const server_down_retry_interval = 5; // minutes
pub const Goal = union(enum) {
gather: GatherGoal,
craft: CraftGoal,
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void {
switch (self.*) {
.gather => |*gather| gather.tick(goal_id, artificer),
.craft => |*craft| try craft.tick(goal_id, artificer),
}
}
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
switch (self.*) {
.gather => |*gather| gather.onActionCompleted(goal_id, result),
.craft => |*craft| craft.onActionCompleted(goal_id, result),
}
}
};
const GoalSlot = struct {
generation: GoalId.Generation = 0,
goal: ?Goal = null,
};
pub const Action = union(enum) {
move: Api.Position,
gather,
craft: struct {
item: Api.Store.Id,
quantity: u64
}
};
pub const ActionResult = union(enum) {
move: Api.MoveResult,
gather: Api.GatherResult,
craft: Api.CraftResult,
};
const ActionSlot = struct {
goal: GoalId,
action: Action,
};
const QueuedActions = std.ArrayListUnmanaged(ActionSlot);
server: *Api.Server,
// characters: std.ArrayList(Brain),
// task_graph: TaskGraph,
character: Api.Store.Id,
goal_slots: []GoalSlot,
queued_actions: QueuedActions,
// paused_until: ?i64 = null, // ms
pub fn init(allocator: Allocator, server: *Api.Server, character: Api.Store.Id) !Artificer {
const max_queued_actions = 16;
pub fn init(allocator: Allocator, server: *Api.Server) !Artificer {
// var characters = std.ArrayList(Brain).init(allocator);
// errdefer characters.deinit(); // TODO: Add character deinit
//
// const chars = try server.listMyCharacters();
// defer chars.deinit();
//
// for (chars.items) |char| {
// try characters.append(try Brain.init(allocator, char.name));
// }
const goal_slots = try allocator.alloc(GoalSlot, max_goals);
errdefer allocator.free(goal_slots);
@memset(goal_slots, .{});
var queued_actions = try QueuedActions.initCapacity(allocator, max_queued_actions);
errdefer queued_actions.deinit(allocator);
_ = allocator;
return Artificer{
.server = server,
// .characters = characters,
// .task_graph = TaskGraph.init(allocator),
.goal_slots = goal_slots,
.character = character,
.queued_actions = queued_actions
};
}
pub fn deinit(self: *Artificer) void {
_ = self;
// for (self.characters.items) |brain| {
// brain.deinit();
// }
// self.characters.deinit();
// self.server.deinit();
pub fn deinit(self: *Artificer, allocator: Allocator) void {
allocator.free(self.goal_slots);
self.queued_actions.deinit(allocator);
}
pub fn appendGoal(self: *Artificer, goal: Goal) !GoalId {
for (0.., self.goal_slots) |index, *goal_slot| {
if (goal_slot.goal != null) {
continue;
}
if (goal_slot.generation == std.math.maxInt(GoalId.Generation)) {
continue;
}
goal_slot.goal = goal;
return GoalId{
.index = @intCast(index),
.generation = goal_slot.generation
};
}
return error.OutOfMemory;
}
pub fn removeGoal(self: *Artificer, id: GoalId) void {
if (self.getGoal(id)) |goal_slot| {
goal_slot.* = .{
.generation = goal_slot.generation + 1
};
}
}
pub fn getGoal(self: *Artificer, id: GoalId) ?*GoalSlot {
const slot = &self.goal_slots[id.index];
if (slot.generation != id.generation) {
return null;
}
if (slot.goal == null) {
return null;
}
return slot;
}
pub fn findBestResourceWithItem(self: *Artificer, item: Api.Store.Id) ?Api.Store.Id {
const store = self.server.store;
const character = store.characters.get(self.character).?;
var best_resource: ?Api.Store.Id = null;
var best_rate: u64 = 0;
for (0.., store.resources.objects.items) |resource_id, optional_resource| {
if (optional_resource != .object) {
continue;
}
const resource = optional_resource.object;
const skill = resource.skill.toCharacterSkill();
const character_skill_level = character.skills.get(skill).level;
if (character_skill_level < resource.level) {
continue;
}
for (resource.drops.slice()) |_drop| {
const drop: Api.Resource.Drop = _drop;
if (drop.item != item) {
continue;
}
// The lower the `drop.rate` the better
if (best_resource == null or best_rate > drop.rate) {
best_resource = resource_id;
best_rate = drop.rate;
break;
}
}
}
return best_resource;
}
pub fn findNearestMapWithResource(self: *Artificer, resource: Api.Store.Id) ?Api.Position {
const store = self.server.store;
const character = store.characters.get(self.character).?;
const resource_code = store.resources.get(resource).?.code.slice();
var nearest_position: ?Api.Position = null;
for (store.maps.items) |map| {
const content = map.content orelse continue;
if (content.type != .resource) {
continue;
}
if (!std.mem.eql(u8, resource_code, content.code.slice())) {
continue;
}
if (nearest_position == null or map.position.distance(character.position) < map.position.distance(nearest_position.?)) {
nearest_position = map.position;
}
}
return nearest_position;
}
pub fn findNearestWorkstation(self: *Artificer, skill: Api.Craft.Skill) ?Api.Position {
const store = self.server.store;
const character = store.characters.get(self.character).?;
const skill_name = skill.toString();
var nearest_position: ?Api.Position = null;
for (store.maps.items) |map| {
const content = map.content orelse continue;
if (content.type != .workshop) {
continue;
}
if (!std.mem.eql(u8, skill_name, content.code.slice())) {
continue;
}
if (nearest_position == null or map.position.distance(character.position) < map.position.distance(nearest_position.?)) {
nearest_position = map.position;
}
}
return nearest_position;
}
fn timeUntilCooldownExpires(self: *Artificer) u64 {
const store = self.server.store;
const character = store.characters.get(self.character).?;
if (character.cooldown_expiration) |cooldown_expiration| {
const cooldown_expiration_ns: i64 = @intFromFloat(cooldown_expiration * std.time.ns_per_s);
const now = std.time.nanoTimestamp();
if (cooldown_expiration_ns > now) {
return @intCast(@as(i128, @intCast(cooldown_expiration_ns)) - now);
}
}
return 0;
}
fn getGoalCount(self: *Artificer) u32 {
var count: u32 = 0;
for (self.goal_slots) |goal_slot| {
if (goal_slot.goal == null) {
continue;
}
count += 1;
}
return count;
}
pub fn tick(self: *Artificer) !void {
_ = self;
const store = self.server.store;
if (self.queued_actions.items.len > 0) {
const expires_in = self.timeUntilCooldownExpires();
if (expires_in > 0) {
return;
}
const character = store.characters.get(self.character).?;
const action_slot = self.queued_actions.orderedRemove(0);
const action_result = switch (action_slot.action) {
.move => |position| ActionResult{
.move = try self.server.move(character.name.slice(), position)
},
.gather => ActionResult{
.gather = try self.server.gather(character.name.slice())
},
.craft => |craft| ActionResult{
.craft = try self.server.craft(character.name.slice(), store.items.getName(craft.item).?, craft.quantity)
}
};
if (self.getGoal(action_slot.goal)) |goal_slot| {
const goal = &goal_slot.goal.?;
goal.onActionCompleted(action_slot.goal, action_result);
}
} else {
for (0.., self.goal_slots) |index, *goal_slot| {
if (goal_slot.goal == null) {
continue;
}
const goal = &(goal_slot.*.goal orelse continue);
const goal_id = GoalId{
.index = @intCast(index),
.generation = goal_slot.generation
};
try goal.tick(goal_id, self);
}
}
}
// pub fn step(self: *Artificer) !void {
// if (self.paused_until) |paused_until| {
// if (std.time.milliTimestamp() < paused_until) {
// return;
// }
// self.paused_until = null;
// }
//
// runNextActions(self.characters.items, &self.server) catch |err| switch (err) {
// Api.FetchError.ServerUnavailable => {
// self.paused_until = std.time.milliTimestamp() + std.time.ms_per_min * server_down_retry_interval;
// std.log.warn("Server is down, retrying in {}min", .{server_down_retry_interval});
// return;
// },
// else => return err,
// };
//
// for (self.characters.items) |*brain| {
// if (brain.task != null) {
// try brain.step(&self.server);
// continue;
// }
//
// const character = self.server.store.getCharacter(brain.name).?;
// if (character.task) |taskmaster_task| {
// if (taskmaster_task.total > taskmaster_task.progress) {
// switch (taskmaster_task.type) {
// .monsters => {
// const monster_code = self.server.store.getCode(taskmaster_task.target_id).?;
//
// const maps = try self.server.getMaps(.{ .code = monster_code });
// defer maps.deinit();
//
// if (maps.items.len > 0) {
// const resource_map: Api.Map = maps.items[0];
// std.debug.print("fight at {}\n", .{resource_map.position});
//
// brain.task = .{ .fight = .{
// .at = resource_map.position,
// .until = .{ .quantity = taskmaster_task.total - taskmaster_task.progress },
// } };
// }
// },
// .crafts => {},
// .resources => {},
// }
// }
// } else {
// brain.task = .{ .accept_task = .{} };
// }
// }
// }
//
// pub fn nextStepAt(self: *Artificer) i64 {
// if (self.paused_until) |paused_until| {
// return paused_until;
// }
//
// return earliestCooldown(self.characters.items, &self.server) orelse 0;
// }
//
// fn earliestCooldown(characters: []Brain, api: *Api.Server) ?i64 {
// var earliest_cooldown: ?i64 = null;
// for (characters) |*brain| {
// if (brain.action_queue.items.len == 0) continue;
//
// const cooldown = brain.cooldown(api);
// if (earliest_cooldown == null or earliest_cooldown.? > cooldown) {
// earliest_cooldown = cooldown;
// }
// }
//
// return earliest_cooldown;
// }
//
// fn runNextActions(characters: []Brain, api: *Api.Server) !void {
// for (characters) |*brain| {
// if (brain.action_queue.items.len == 0) continue;
//
// const cooldown = brain.cooldown(api);
// if (std.time.milliTimestamp() >= cooldown) {
// try brain.performNextAction(api);
// }
// }
// }
pub fn runUntilGoalsComplete(self: *Artificer) !void {
while (self.getGoalCount() > 0) {
const expires_in = self.timeUntilCooldownExpires();
if (expires_in > 0) {
log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
std.time.sleep(expires_in);
}
try self.tick();
}
}
pub fn runForever(self: *Artificer) !void {
while (true) {
const expires_in = self.timeUntilCooldownExpires();
if (expires_in > 0) {
log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s });
std.time.sleep(expires_in);
log.debug("Finished sleeping", .{});
}
try self.tick();
}
}

View File

@ -1,302 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Position = Api.Position;
const Action = @import("./action.zig").Action;
const ActionResult = @import("./action.zig").ActionResult;
const CodeId = Api.CodeId;
const ItemQuantity = Api.ItemQuantity;
const bank_position = Position{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
const task_master_position = Position{ .x = 1, .y = 2 }; // TODO: Figure this out dynamically
pub const UntilCondition = union(enum) {
xp: u64,
item: Api.ItemQuantity,
quantity: u64,
fn isComplete(self: UntilCondition, progress: u64) bool {
return switch (self) {
.xp => |xp| progress >= xp,
.item => |item| progress >= item.quantity,
.quantity => |quantity| progress >= quantity,
};
}
};
pub const Task = union(enum) {
fight: struct {
at: Position,
until: UntilCondition,
progress: u64 = 0,
},
gather: struct {
at: Position,
until: UntilCondition,
progress: u64 = 0,
},
craft: struct {
at: Position,
target: Api.ItemQuantity,
progress: u64 = 0,
},
accept_task: struct {
done: bool = false
},
pub fn isComplete(self: Task) bool {
return switch (self) {
.fight => |args| args.until.isComplete(args.progress),
.gather => |args| args.until.isComplete(args.progress),
.craft => |args| args.progress >= args.target.quantity,
.accept_task => |args| args.done
};
}
pub fn onActionCompleted(self: *Task, result: ActionResult) void {
switch (self.*) {
.fight => |*args| {
if (result.get(.fight)) |r| {
const fight_result: Api.Server.FightResult = r;
switch (args.until) {
.xp => {
args.progress += fight_result.fight.xp;
},
.item => {
const drops = fight_result.fight.drops;
args.progress += drops.getQuantity(args.until.item.id);
},
.quantity => {
args.progress += 1;
}
}
}
},
.gather => |*args| {
if (result.get(.gather)) |r| {
const gather_result: Api.Server.GatherResult = r;
switch (args.until) {
.xp => {
args.progress += gather_result.details.xp;
},
.item => {
const items = gather_result.details.items;
args.progress += items.getQuantity(args.until.item.id);
},
.quantity => {
args.progress += 1;
}
}
}
},
.craft => |*args| {
if (result.get(.craft_item)) |r| {
const craft_result: Api.Server.CraftResult = r;
const items = craft_result.details.items;
args.progress += items.getQuantity(args.target.id);
}
},
.accept_task => |*args| {
if (result.get(.accept_task)) |_| {
args.done = true;
}
}
}
}
pub fn queueActions(self: Task, api: *Api.Server, name: []const u8, action_queue: *std.ArrayList(Action)) !void {
const ctx = TaskContext{
.api = api,
.name = name,
.action_queue = action_queue
};
switch (self) {
.fight => |args| {
try ctx.fightRoutine(args.at);
},
.gather => |args| {
try ctx.gatherRoutine(args.at);
},
.craft => |args| {
try ctx.craftRoutine(args.at, args.target.id, args.target.quantity);
},
.accept_task => {
if (try ctx.moveIfNeeded(task_master_position)) {
return;
}
try ctx.action_queue.append(.{ .accept_task = {} });
}
}
}
};
const TaskContext = struct {
api: *Api.Server,
name: []const u8,
action_queue: *std.ArrayList(Action),
fn getCharacter(self: TaskContext) Api.Character {
return self.api.store.getCharacter(self.name).?;
}
fn moveIfNeeded(self: TaskContext, pos: Position) !bool {
const character = self.getCharacter();
if (character.position.eql(pos)) {
return false;
}
try self.action_queue.append(.{ .move = pos });
return true;
}
pub fn depositItemsToBank(self: TaskContext) !bool {
var character = self.getCharacter();
const action_queue = self.action_queue;
// Deposit items and gold to bank if full
if (character.getItemCount() == 0) {
return false;
}
if (!character.position.eql(bank_position)) {
try action_queue.append(.{ .move = bank_position });
}
for (character.inventory.slice()) |slot| {
try action_queue.append(.{
.deposit_item = .{ .id = slot.id, .quantity = slot.quantity }
});
}
return true;
}
fn depositIfFull(self: TaskContext) !bool {
const character = self.getCharacter();
if (character.getItemCount() < character.inventory_max_items) {
return false;
}
_ = try depositItemsToBank(self);
if (character.gold > 0) {
try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
}
return true;
}
fn fightRoutine(self: TaskContext, enemy_position: Position) !void {
if (try self.depositIfFull()) {
return;
}
if (try self.moveIfNeeded(enemy_position)) {
return;
}
try self.action_queue.append(.{ .fight = {} });
}
fn gatherRoutine(self: TaskContext, resource_position: Position) !void {
if (try self.depositIfFull()) {
return;
}
if (try self.moveIfNeeded(resource_position)) {
return;
}
try self.action_queue.append(.{ .gather = {} });
}
fn withdrawFromBank(self: TaskContext, items: []const ItemQuantity) !bool {
const character = self.getCharacter();
var has_all_items = true;
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
has_all_items = false;
break;
}
}
if (has_all_items) return false;
if (try self.moveIfNeeded(bank_position)) {
return true;
}
for (items) |item_quantity| {
const inventory_quantity = character.inventory.getQuantity(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
try self.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id,
.quantity = item_quantity.quantity - inventory_quantity,
}});
}
}
return true;
}
fn craftItem(self: TaskContext, workstation: Position, id: CodeId, quantity: u64) !bool {
var character = self.getCharacter();
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
return false;
}
if (try self.moveIfNeeded(workstation)) {
return true;
}
try self.action_queue.append(.{ .craft_item = .{
.id = id,
.quantity = quantity - inventory_quantity
}});
return true;
}
fn craftRoutine(self: TaskContext, workstation: Position, id: CodeId, quantity: u64) !void {
var character = self.getCharacter();
const inventory_quantity = character.inventory.getQuantity(id);
if (inventory_quantity >= quantity) {
if (try self.depositItemsToBank()) {
return;
}
}
const code = self.api.store.getCode(id) orelse return error.InvalidItemId;
const target_item = try self.api.getItem(code) orelse return error.ItemNotFound;
if (target_item.craft == null) {
return error.NotCraftable;
}
const recipe = target_item.craft.?;
var needed_items = recipe.items;
for (needed_items.slice()) |*needed_item| {
needed_item.quantity *= quantity;
}
if (try self.withdrawFromBank(needed_items.slice())) {
return;
}
if (try self.craftItem(workstation, id, quantity)) {
return;
}
}
};

View File

@ -1,197 +0,0 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
const TaskGraph = @This();
const CharacterTask = @import("./task.zig").Task;
const TaskNodeId = u16;
const TaskNode = struct {
const Dependencies = std.BoundedArray(TaskNodeId, 8);
const MissingItems = Api.BoundedSlotsArray(8);
task: CharacterTask,
dependencies: Dependencies = Dependencies.init(0) catch unreachable,
missing_items: MissingItems = MissingItems.init(),
};
const Nodes = std.ArrayList(TaskNode);
nodes: Nodes,
pub fn init(allocator: Allocator) TaskGraph {
return TaskGraph{ .nodes = Nodes.init(allocator) };
}
pub fn deinit(self: TaskGraph) void {
self.nodes.deinit();
}
fn get(self: *TaskGraph, id: TaskNodeId) *TaskNode {
return &self.nodes.items[id];
}
fn addTask(self: *TaskGraph, node: TaskNode) !TaskNodeId {
try self.nodes.append(node);
return @intCast(self.nodes.items.len-1);
}
fn addFightTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
const monsters = try api.getMonsters(.{ .drop = item_code });
defer monsters.deinit();
if (monsters.items.len == 0) return error.ResourceNotFound;
if (monsters.items.len > 1) std.log.warn("Multiple monsters exist for target item", .{});
const monster_code = monsters.items[0].code;
const resource_maps = try self.api.getMaps(.{ .code = monster_code });
defer resource_maps.deinit();
// This monster currently doesn't exist on the map. Probably only spawns in certain situations.
if (resource_maps.items.len == 0) return error.MapNotFound;
if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for monster", .{});
const resource_map = resource_maps.items[0];
return try self.addTask(TaskNode{
.task = .{
.fight = .{
.at = resource_map.position,
.until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
}
}
});
}
fn addGatherTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const item_code = api.store.getCode(item_id) orelse return error.ItemNotFound;
const resources = try api.getResources(.{ .drop = item_code });
defer resources.deinit();
if (resources.items.len == 0) return error.ResourceNotFound;
if (resources.items.len > 1) std.log.warn("Multiple resources exist for target item", .{});
const resource_code = resources.items[0].code;
const resource_maps = try self.api.getMaps(.{ .code = resource_code });
defer resource_maps.deinit();
if (resource_maps.items.len == 0) return error.MapNotFound;
if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for resource", .{});
const resource_map = resource_maps.items[0];
return try self.addTask(TaskNode{
.task = .{
.gather = .{
.at = resource_map.position,
.until = .{ .item = Api.ItemQuantity.init(item_id, quantity) }
}
}
});
}
fn addCraftTaskShallow(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
const recipe = item.craft orelse return error.RecipeNotFound;
const skill_str = Api.Server.SkillUtils.toString(recipe.skill);
const workshop_maps = try self.api.getMaps(.{ .code = skill_str, .type = .workshop });
defer workshop_maps.deinit();
if (workshop_maps.items.len == 0) return error.WorkshopNotFound;
if (workshop_maps.items.len > 1) std.log.warn("Multiple workshop locations exist", .{});
return try self.addTask(TaskNode{
.task = .{
.craft = .{
.at = workshop_maps.items[0].position,
.target = Api.ItemQuantity.init(item_id, quantity)
}
}
});
}
fn addCraftTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) !TaskNodeId {
const node_id = try self.addCraftTaskShallow(api, item_id, quantity);
var node = self.get(node_id);
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
const recipe = item.craft orelse return error.RecipeNotFound;
const craft_count = recipe.quantity;
for (recipe.items.slots.constSlice()) |material| {
const needed_quantity = material.quantity * craft_count;
if (try self.addAutoTask(api, material.id, needed_quantity)) |dependency_id| {
try node.dependencies.append(dependency_id);
} else {
try node.missing_items.add(material.id, needed_quantity);
}
}
return node_id;
}
// TODO: Remove `anyerror` from function declaration
fn addAutoTask(self: *TaskGraph, api: *Api.Server, item_id: Api.CodeId, quantity: u64) anyerror!?TaskNodeId {
const item = (try self.api.getItemById(item_id)) orelse return error.ItemNotFound;
if (item.craft != null) {
return try self.addCraftTask(api, item_id, quantity);
} else if (item.type == .resource) {
const eql = std.mem.eql;
if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) {
return try self.addGatherTask(api, item_id, quantity);
} else if (eql(u8, item.subtype, "mob")) {
return try self.addFightTask(api, item_id, quantity);
}
}
return null;
}
fn printTask(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId) void {
self.printTaskLevel(api, node_id, 0);
}
fn writeIdentation(level: u32) void {
const mutex = std.debug.getStderrMutex();
mutex.lock();
defer mutex.unlock();
const stderr = std.io.getStdErr().writer();
stderr.writeBytesNTimes(" ", level) catch return;
}
fn printTaskLevel(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId, level: u32) void {
const node = self.get(node_id);
const print = std.debug.print;
writeIdentation(level);
switch (node.task) {
.fight => |args| {
const target_item = args.until.item;
const item = api.store.getCode(target_item.id).?;
print("Task{{ .fight = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
},
.gather => |args| {
const target_item = args.until.item;
const item = api.store.getCode(target_item.id).?;
print("Task{{ .gather = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
},
.craft => |args| {
const item = api.store.getCode(args.target.id).?;
print("Task{{ .craft = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at});
},
}
for (node.dependencies.constSlice()) |dependency| {
self.printTaskLevel(dependency, level + 1);
}
for (node.missing_items.slots.constSlice()) |slot| {
const item_code = api.getItemCode(slot.id).?;
writeIdentation(level+1);
print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity});
}
}