diff --git a/api/root.zig b/api/root.zig index 5ccb319..af9f777 100644 --- a/api/root.zig +++ b/api/root.zig @@ -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"); diff --git a/api/schemas/character.zig b/api/schemas/character.zig index 8c01b8a..d3b02fe 100644 --- a/api/schemas/character.zig +++ b/api/schemas/character.zig @@ -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 }; } diff --git a/api/schemas/cooldown.zig b/api/schemas/cooldown.zig new file mode 100644 index 0000000..8c61ba9 --- /dev/null +++ b/api/schemas/cooldown.zig @@ -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 + }; +} diff --git a/api/schemas/craft.zig b/api/schemas/craft.zig index 385b3a1..1633c82 100644 --- a/api/schemas/craft.zig +++ b/api/schemas/craft.zig @@ -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, diff --git a/api/schemas/equipment.zig b/api/schemas/equipment.zig index 6f34f08..55cf965 100644 --- a/api/schemas/equipment.zig +++ b/api/schemas/equipment.zig @@ -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, + }; +} diff --git a/api/schemas/ge_order.zig b/api/schemas/ge_order.zig new file mode 100644 index 0000000..9d565d1 --- /dev/null +++ b/api/schemas/ge_order.zig @@ -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)); +} diff --git a/api/schemas/move_result.zig b/api/schemas/move_result.zig new file mode 100644 index 0000000..800be0f --- /dev/null +++ b/api/schemas/move_result.zig @@ -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; +} diff --git a/api/schemas/position.zig b/api/schemas/position.zig index 712f640..cd56ea2 100644 --- a/api/schemas/position.zig +++ b/api/schemas/position.zig @@ -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, diff --git a/api/schemas/resource.zig b/api/schemas/resource.zig index fba8cee..7ddce12 100644 --- a/api/schemas/resource.zig +++ b/api/schemas/resource.zig @@ -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, diff --git a/api/schemas/skill_info_details.zig b/api/schemas/skill_info_details.zig new file mode 100644 index 0000000..45512db --- /dev/null +++ b/api/schemas/skill_info_details.zig @@ -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) + }; +} diff --git a/api/schemas/skill_usage_result.zig b/api/schemas/skill_usage_result.zig new file mode 100644 index 0000000..fd95c4f --- /dev/null +++ b/api/schemas/skill_usage_result.zig @@ -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; +} diff --git a/api/server.zig b/api/server.zig index 1199ecc..a055ddb 100644 --- a/api/server.zig +++ b/api/server.zig @@ -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; diff --git a/api/store.zig b/api/store.zig index 927fd41..6ea58d1 100644 --- a/api/store.zig +++ b/api/store.zig @@ -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 { diff --git a/cli/main.zig b/cli/main.zig index f052c74..3d953c3 100644 --- a/cli/main.zig +++ b/cli/main.zig @@ -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(); } diff --git a/lib/action.zig b/lib/action.zig deleted file mode 100644 index 710a3ab..0000000 --- a/lib/action.zig +++ /dev/null @@ -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); -} diff --git a/lib/brain.zig b/lib/brain.zig deleted file mode 100644 index 5d3d106..0000000 --- a/lib/brain.zig +++ /dev/null @@ -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; - } -} diff --git a/lib/craft_goal.zig b/lib/craft_goal.zig new file mode 100644 index 0000000..debf948 --- /dev/null +++ b/lib/craft_goal.zig @@ -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; + } + } +} diff --git a/lib/gather_goal.zig b/lib/gather_goal.zig new file mode 100644 index 0000000..36c65fe --- /dev/null +++ b/lib/gather_goal.zig @@ -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; + } + } +} + diff --git a/lib/root.zig b/lib/root.zig index c1cb912..3b4c98d 100644 --- a/lib/root.zig +++ b/lib/root.zig @@ -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(); + } +} diff --git a/lib/task.zig b/lib/task.zig deleted file mode 100644 index 53ad338..0000000 --- a/lib/task.zig +++ /dev/null @@ -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; - } - } -}; - diff --git a/lib/task_graph.zig b/lib/task_graph.zig deleted file mode 100644 index 002a93f..0000000 --- a/lib/task_graph.zig +++ /dev/null @@ -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}); - } -}