diff --git a/src/artifacts.zig b/src/artifacts.zig index 41b41f3..4416634 100644 --- a/src/artifacts.zig +++ b/src/artifacts.zig @@ -160,6 +160,38 @@ pub const EquipmentSlot = enum { } }; +pub const Skill = enum { + weaponcrafting, + gearcrafting, + jewelrycrafting, + cooking, + woodcutting, + mining, + + fn parse(str: []const u8) ?Skill { + const eql = std.mem.eql; + const mapping = .{ + .{ "weaponcrafting" , .weaponcrafting }, + .{ "gearcrafting" , .gearcrafting }, + .{ "jewelrycrafting", .jewelrycrafting }, + .{ "cooking" , .cooking }, + .{ "woodcutting" , .woodcutting }, + .{ "mining" , .mining }, + }; + if (mapping.len != @typeInfo(Skill).Enum.fields.len) { + @compileLog("Mapping is not exhaustive"); + } + + inline for (mapping) |mapping_entry| { + if (eql(u8, str, mapping_entry[0])) { + return mapping_entry[1]; + } + } + + return null; + } +}; + const ServerStatus = struct { allocator: Allocator, status: []const u8, @@ -250,35 +282,32 @@ pub const Cooldown = struct { fn parse(str: []const u8) ?Reason { const eql = std.mem.eql; - if (eql(u8, str, "movement")) { - return .movement; - } else if (eql(u8, str, "fight")) { - return .fight; - } else if (eql(u8, str, "crafting")) { - return .crafting; - } else if (eql(u8, str, "gathering")) { - return .gathering; - } else if (eql(u8, str, "buy_ge")) { - return .buy_ge; - } else if (eql(u8, str, "sell_ge")) { - return .sell_ge; - } else if (eql(u8, str, "delete_item")) { - return .delete_item; - } else if (eql(u8, str, "deposit_bank")) { - return .deposit_bank; - } else if (eql(u8, str, "withdraw_bank")) { - return .withdraw_bank; - } else if (eql(u8, str, "equip")) { - return .equip; - } else if (eql(u8, str, "unequip")) { - return .unequip; - } else if (eql(u8, str, "task")) { - return .task; - } else if (eql(u8, str, "recycling")) { - return .recycling; - } else { - return null; + const mapping = .{ + .{ "movement" , .movement }, + .{ "fight" , .fight }, + .{ "crafting" , .crafting }, + .{ "gathering" , .gathering }, + .{ "buy_ge" , .buy_ge }, + .{ "sell_ge" , .sell_ge }, + .{ "delete_item" , .delete_item }, + .{ "deposit_bank" , .deposit_bank }, + .{ "withdraw_bank", .withdraw_bank }, + .{ "equip" , .equip }, + .{ "unequip" , .unequip }, + .{ "task" , .task }, + .{ "recycling" , .recycling }, + }; + if (mapping.len != @typeInfo(Reason).Enum.fields.len) { + @compileLog("Mapping is not exhaustive"); } + + inline for (mapping) |mapping_entry| { + if (eql(u8, str, mapping_entry[0])) { + return mapping_entry[1]; + } + } + + return null; } }; @@ -365,44 +394,52 @@ pub const FightResult = struct { } }; -pub const ItemQuantity = struct { +pub const ItemIdQuantity = struct { id: ItemId, quantity: u64, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemQuantity { + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemIdQuantity { const code = try json_utils.getStringRequired(obj, "code"); const quantity = try json_utils.getIntegerRequired(obj, "quantity"); + if (quantity < 1) return error.InvalidQuantity; - return ItemQuantity{ + return ItemIdQuantity{ .id = try api.getItemId(code), .quantity = @intCast(quantity) }; } }; -pub const SkillDetails = struct { +const BoundedItems = std.BoundedArray(ItemIdQuantity, 8); +fn parseSimpleItemList(api: *ArtifactsAPI, array: json.Array) !BoundedItems { + var items = BoundedItems.init(0) catch unreachable; + + for (array.items) |item_value| { + const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty; + + try items.append(try ItemIdQuantity.parse(api, item_obj)); + } + + return items; +} + +pub const SkillResultDetails = struct { xp: i64, - items: std.BoundedArray(ItemQuantity, 8), + items: BoundedItems, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !SkillDetails { - var items = std.BoundedArray(ItemQuantity, 8).init(0) catch unreachable; - const items_obj = json_utils.getArray(obj, "items") orelse return error.MissingProperty; - for (items_obj.items) |item_value| { - const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty; + fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !SkillResultDetails { + const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty; - try items.append(ItemQuantity.parse(api, item_obj)); - } - - return SkillDetails{ + return SkillResultDetails{ .xp = try json_utils.getIntegerRequired(obj, "xp"), - .items = items, + .items = try parseSimpleItemList(api, items), }; } }; pub const GatherResult = struct { cooldown: Cooldown, - details: SkillDetails, + details: SkillResultDetails, pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GatherResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; @@ -410,7 +447,7 @@ pub const GatherResult = struct { return GatherResult{ .cooldown = try Cooldown.parse(cooldown), - .details = try SkillDetails.parse(api, details) + .details = try SkillResultDetails.parse(api, details) }; } @@ -531,15 +568,11 @@ pub const ItemTransactionResult = struct { else => null }; } - - pub fn deinit(self: ItemTransactionResult) void { - _ = self; - } }; pub const CraftResult = struct { cooldown: Cooldown, - details: SkillDetails, + details: SkillResultDetails, pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !CraftResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; @@ -547,7 +580,7 @@ pub const CraftResult = struct { return CraftResult{ .cooldown = try Cooldown.parse(cooldown), - .details = try SkillDetails.parse(api, details) + .details = try SkillResultDetails.parse(api, details) }; } @@ -670,6 +703,72 @@ pub const MapResult = struct { } }; +pub const Item = struct { + pub const Recipe = struct { + skill: Skill, + level: u64, + quantity: u64, + items: BoundedItems, + + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Recipe { + const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty; + const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty; + if (level < 1) return error.InvalidLevel; + + const quantity = json_utils.getInteger(obj, "quantity") orelse return error.MissingProperty; + if (quantity < 1) return error.InvalidQuantity; + + const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty; + + return Recipe{ + .skill = Skill.parse(skill) orelse return error.InvalidSkill, + .level = @intCast(level), + .quantity = @intCast(quantity), + .items = try parseSimpleItemList(api, items) + }; + } + }; + + allocator: Allocator, + name: []u8, + code: []u8, + level: u64, + type: []u8, + subtype: []u8, + description: []u8, + craft: ?Recipe, + // TODO: effects + // TODO: Grand exchange + + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !Item { + const item_obj = json_utils.getObject(obj, "item") orelse return error.MissingProperty; + + const level = json_utils.getInteger(item_obj, "level") orelse return error.MissingProperty; + if (level < 1) return error.InvalidLevel; + + const craft = json_utils.getObject(item_obj, "craft"); + + return Item{ + .allocator = allocator, + .name = (try json_utils.dupeString(allocator, item_obj, "name")) orelse return error.MissingProperty, + .code = (try json_utils.dupeString(allocator, item_obj, "code")) orelse return error.MissingProperty, + .level = @intCast(level), + .type = (try json_utils.dupeString(allocator, item_obj, "type")) orelse return error.MissingProperty, + .subtype = (try json_utils.dupeString(allocator, item_obj, "subtype")) orelse return error.MissingProperty, + .description = (try json_utils.dupeString(allocator, item_obj, "description")) orelse return error.MissingProperty, + .craft = if (craft != null) try Recipe.parse(api, craft.?) else null + }; + } + + pub fn deinit(self: Item) void { + self.allocator.free(self.name); + self.allocator.free(self.code); + self.allocator.free(self.type); + self.allocator.free(self.subtype); + self.allocator.free(self.description); + } +}; + pub const ArtifactsFetchResult = struct { arena: std.heap.ArenaAllocator, status: std.http.Status, @@ -711,6 +810,8 @@ const FetchOptions = struct { path: []const u8, payload: ?[]const u8 = null, + query: ?[]const u8 = null, + page: ?u64 = null, page_size: ?u64 = null, paginated: bool = false @@ -743,12 +844,23 @@ fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResu var fetch_results = std.ArrayList(json.Value).init(arena.allocator()); while (true) : (current_page += 1) { - var query_params: ?[]u8 = null; - defer if (query_params) |str| self.allocator.free(str); + var pagination_params: ?[]u8 = null; + defer if (pagination_params) |str| self.allocator.free(str); if (options.paginated) { - query_params = try allocPaginationParams(self.allocator, current_page, options.page_size); - uri.query = .{ .raw = query_params.? }; + pagination_params = try allocPaginationParams(self.allocator, current_page, options.page_size); + } + + if (options.query != null and pagination_params != null) { + const combined = try std.mem.join(self.allocator, "&", &.{ options.query.?, pagination_params.? }); + self.allocator.free(pagination_params.?); + pagination_params = combined; + + uri.query = .{ .raw = combined }; + } else if (pagination_params != null) { + uri.query = .{ .raw = pagination_params.? }; + } else if (options.query != null) { + uri.query = .{ .raw = options.query.? }; } var response_storage = std.ArrayList(u8).init(arena.allocator()); @@ -791,6 +903,7 @@ fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResu if (options.paginated) { const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return APIError.ParseFailed; + if (total_pages_i64 < 1) return APIError.ParseFailed; total_pages = @intCast(total_pages_i64); const page_results = json_utils.getArray(parsed.object, "data") orelse return APIError.ParseFailed; @@ -835,13 +948,6 @@ fn handleFetchError( } } - if (status == .not_found) { - return null; - } - if (status != .ok) { - return APIError.RequestFailed; - } - return null; } @@ -864,6 +970,12 @@ fn fetchOptionalObject( if (handleFetchError(result.status, Error, parseError)) |error_value| { return error_value; } + if (result.status == .not_found) { + return null; + } + if (result.status != .ok) { + return APIError.RequestFailed; + } if (result.body == null) { return APIError.ParseFailed; @@ -886,7 +998,26 @@ fn fetchObject( return result orelse return APIError.RequestFailed; } -fn fetchArray( +fn ObjectList(Object: type) type { + return struct { + list: std.ArrayList(Object), + + pub fn deinit(self: @This()) void { + for (self.list.items) |*item| { + if (std.meta.hasMethod(@TypeOf(item), "deinit")) { + item.deinit(); + } + } + self.deinitList(); + } + + pub fn deinitList(self: @This()) void { + self.list.deinit(); + } + }; +} + +fn fetchOptionalArray( self: *ArtifactsAPI, allocator: Allocator, Error: type, @@ -895,7 +1026,7 @@ fn fetchArray( parseObject: anytype, parseObjectArgs: anytype, fetchOptions: FetchOptions -) Error!std.ArrayList(Object) { +) Error!?ObjectList(Object) { if (@typeInfo(@TypeOf(parseObject)) != .Fn) { @compileError("`parseObject` must be a function"); } @@ -906,23 +1037,56 @@ fn fetchArray( if (handleFetchError(result.status, Error, parseError)) |error_value| { return error_value; } + if (result.status == .not_found) { + return null; + } + if (result.status != .ok) { + return APIError.RequestFailed; + } if (result.body == null) { return APIError.ParseFailed; } - var array = std.ArrayList(Object).init(allocator); - errdefer array.deinit(); + var object_array = ObjectList(Object){ + .list = std.ArrayList(Object).init(allocator) + }; + errdefer object_array.deinit(); + // var array = std.ArrayList(Object).init(allocator); + // errdefer { + // if (std.meta.hasFn(Object, "deinit")) { + // for (array.items) |item| { + // if (@typeInfo(Object.deinit).Fn.args.len == 1) { + // item.deinit(); + // } + // } + // } + // array.deinit(); + // } const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed; for (result_data.items) |result_item| { const item_obj = json_utils.asObject(result_item) orelse return APIError.ParseFailed; const parsed_item = @call(.auto, parseObject, .{ self, item_obj } ++ parseObjectArgs) catch return APIError.ParseFailed; - array.append(parsed_item) catch return APIError.OutOfMemory; + object_array.list.append(parsed_item) catch return APIError.OutOfMemory; } - return array; + return object_array; +} + +fn fetchArray( + self: *ArtifactsAPI, + allocator: Allocator, + Error: type, + parseError: ?fn (status: std.http.Status) ?Error, + Object: type, + parseObject: anytype, + parseObjectArgs: anytype, + fetchOptions: FetchOptions +) Error!ObjectList(Object) { + const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions); + return result orelse return APIError.RequestFailed; } pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void { @@ -995,7 +1159,7 @@ pub fn getCharacter(self: *ArtifactsAPI, allocator: Allocator, name: []const u8) ); } -pub fn listMyCharacters(self: *ArtifactsAPI, allocator: Allocator) APIError!std.ArrayList(Character) { +pub fn listMyCharacters(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Character) { return self.fetchArray( allocator, APIError, @@ -1205,20 +1369,48 @@ pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 { const data = json_utils.asObject(result.body.?) orelse return APIError.RequestFailed; const quantity = json_utils.getInteger(data, "quantity") orelse return APIError.ParseFailed; + if (quantity < 0) return APIError.ParseFailed; + return @intCast(quantity); } -pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!std.ArrayList(ItemQuantity) { +pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(ItemIdQuantity) { return self.fetchArray( allocator, APIError, null, - ItemQuantity, - ItemQuantity.parse, .{}, + ItemIdQuantity, + ItemIdQuantity.parse, .{}, .{ .method = .GET, .path = "/my/bank/items", .paginated = true } ); } +pub fn getBankItemQuantity(self: *ArtifactsAPI, code: []const u8) APIError!?u64 { + const query = try std.fmt.allocPrint(self.allocator, "item_code={s}", .{code}); + defer self.allocator.free(query); + + const maybe_items = try self.fetchOptionalArray( + self.allocator, + APIError, + null, + ItemIdQuantity, + ItemIdQuantity.parse, .{}, + .{ .method = .GET, .path = "/my/bank/items", .query = query, .paginated = true } + ); + if (maybe_items == null) { + return null; + } + + const items = maybe_items.?; + defer items.deinit(); + + const list_items = items.list.items; + assert(list_items.len == 1); + assert(list_items[0].id == try self.getItemId(code)); + + return list_items[0].quantity; +} + pub fn getMap(self: *ArtifactsAPI, allocator: Allocator, x: i64, y: i64) APIError!?MapResult { const path = try std.fmt.allocPrint(self.allocator, "/maps/{}/{}", .{x, y}); defer self.allocator.free(path); @@ -1243,6 +1435,19 @@ pub fn getMaps(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Ma ); } +pub fn getItem(self: *ArtifactsAPI, allocator: Allocator, code: []const u8) APIError!?Item { + const path = try std.fmt.allocPrint(self.allocator, "/items/{s}", .{code}); + defer self.allocator.free(path); + + return self.fetchOptionalObject( + APIError, + null, + Item, + Item.parse, .{ allocator }, + .{ .method = .GET, .path = path } + ); +} + test "parse date time" { try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?); } diff --git a/src/character.zig b/src/character.zig index 1d46d47..1744595 100644 --- a/src/character.zig +++ b/src/character.zig @@ -102,12 +102,15 @@ pub const Inventory = struct { pub const Slot = struct { id: ?ItemId, - quantity: i64, + quantity: u64, fn parse(api: *ArtifactsAPI, slot_obj: json.ObjectMap) !Slot { + const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity"); + if (quantity < 0) return error.InvalidQuantity; + return Slot{ .id = try getItemId(api, slot_obj, "code"), - .quantity = try json_utils.getIntegerRequired(slot_obj, "quantity"), + .quantity = @intCast(quantity), }; } }; @@ -140,7 +143,7 @@ pub const Inventory = struct { const slot = self.findSlot(id) orelse unreachable; assert(slot.quantity >= quantity); - slot.quantity -= @intCast(quantity); + slot.quantity -= quantity; if (slot.quantity == 0) { slot.id = null; } @@ -148,7 +151,7 @@ pub const Inventory = struct { pub fn addItem(self: *Inventory, id: ItemId, quantity: u64) void { if (self.findSlot(id)) |slot| { - slot.quantity += @intCast(quantity); + slot.quantity += quantity; } else { var empty_slot: ?*Slot = null; for (&self.slots) |*slot| { @@ -159,9 +162,29 @@ pub const Inventory = struct { assert(empty_slot != null); empty_slot.?.id = id; - empty_slot.?.quantity = @intCast(quantity); + empty_slot.?.quantity = quantity; } } + + pub fn addItems(self: *Inventory, items: []const ArtifactsAPI.ItemIdQuantity) void { + for (items) |item| { + self.addItem(item.id, item.quantity); + } + } + + pub fn removeItems(self: *Inventory, items: []const ArtifactsAPI.ItemIdQuantity) void { + for (items) |item| { + self.removeItem(item.id, item.quantity); + } + } + + pub fn getItem(self: *Inventory, id: ItemId) u64 { + if (self.findSlot(id)) |slot| { + return slot.quantity; + } + + return 0; + } }; arena: *std.heap.ArenaAllocator, @@ -249,10 +272,10 @@ pub fn deinit(self: *Character) void { child_allocator.destroy(self.arena); } -pub fn getItemCount(self: *const Character) u32 { - var count: u32 = 0; +pub fn getItemCount(self: *const Character) u64 { + var count: u64 = 0; for (self.inventory.slots) |slot| { - count += @intCast(slot.quantity); + count += slot.quantity; } return count; } diff --git a/src/main.zig b/src/main.zig index 28c2268..ca0831d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -25,8 +25,10 @@ const QueuedAction = union(enum) { move: Position, attack, gather, - depositGold: u64, - depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 }, + deposit_gold: u64, + deposit_item: ArtifactsAPI.ItemIdQuantity, + withdraw_item: ArtifactsAPI.ItemIdQuantity, + craft_item: ArtifactsAPI.ItemIdQuantity, }; const ActionQueue = std.ArrayList(QueuedAction); @@ -35,6 +37,10 @@ const ManagedCharacter = struct { character: ArtifactsAPI.Character, action_queue: ActionQueue, cooldown_expires_at: f64, + + pub fn position(self: *const ManagedCharacter) Position { + return Position{ .x = self.character.x, .y = self.character.y }; + } }; fn currentTime() f64 { @@ -46,11 +52,15 @@ const Manager = struct { allocator: Allocator, characters: std.ArrayList(ManagedCharacter), api: *ArtifactsAPI, + known_items: std.StringHashMap(ArtifactsAPI.Item), + + bank_position: Position = .{ .x = 4, .y = 1 }, fn init(allocator: Allocator, api: *ArtifactsAPI) Manager { return Manager{ .allocator = allocator, .api = api, + .known_items = std.StringHashMap(ArtifactsAPI.Item).init(allocator), .characters = std.ArrayList(ManagedCharacter).init(allocator), }; } @@ -92,28 +102,74 @@ const Manager = struct { return null; } - fn deinit(self: Manager) void { + fn getItem(self: *Manager, code: []const u8) !?ArtifactsAPI.Item { + if (self.known_items.get(code)) |item| { + return item; + } + + const maybe_item = try self.api.getItem(self.allocator, code); + if (maybe_item == null) { + std.log.warn("attempt to get item '{s}' which does not exist", .{code}); + return null; + } + + const item = maybe_item.?; + try self.known_items.putNoClobber(item.code, item); + + return item; + } + + fn deinit(self: *Manager) void { for (self.characters.items) |managed_character| { managed_character.action_queue.deinit(); } self.characters.deinit(); + + var known_items_iter = self.known_items.valueIterator(); + while (known_items_iter.next()) |item| { + item.deinit(); + } + self.known_items.deinit(); + } + + fn getWorkstation(self: *const Manager, skill: ArtifactsAPI.Skill) Position { + _ = self; + // TODO: Find workstation using map endpoint + + return switch (skill) { + .weaponcrafting => Position{ .x = 2, .y = 1 }, + .gearcrafting => Position{ .x = 3, .y = 1 }, + .jewelrycrafting => Position{ .x = 1, .y = 3 }, + .cooking => Position{ .x = 1, .y = 1 }, + .woodcutting => Position{ .x = -2, .y = -3 }, + .mining => Position{ .x = 1, .y = 5 }, + }; } }; -fn depositIfFull(managed_char: *ManagedCharacter) !bool { +fn moveIfNeeded(char: *ManagedCharacter, pos: Position) !bool { + if (char.position().eql(pos)) { + return false; + } + + try char.action_queue.append(.{ .move = pos }); + + return true; +} + +fn depositItemsToBank(manager: *Manager, managed_char: *ManagedCharacter) !bool { const character = managed_char.character; const action_queue = &managed_char.action_queue; // Deposit items and gold to bank if full - if (character.getItemCount() < character.inventory_max_items) { + if (character.getItemCount() == 0) { return false; } - var character_pos = Position.init(character.x, character.y); - const bank_pos = Position{ .x = 4, .y = 1 }; + var character_pos = managed_char.position(); - if (!character_pos.eql(bank_pos)) { - try action_queue.append(.{ .move = bank_pos }); + if (!character_pos.eql(manager.bank_position)) { + try action_queue.append(.{ .move = manager.bank_position }); } for (character.inventory.slots) |slot| { @@ -121,58 +177,140 @@ fn depositIfFull(managed_char: *ManagedCharacter) !bool { if (slot.id) |item_id| { try action_queue.append(.{ - .depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) } + .deposit_item = .{ .id = item_id, .quantity = @intCast(slot.quantity) } }); } } - if (character.gold > 0) { - try action_queue.append(.{ .depositGold = @intCast(character.gold) }); - } - return true; } -fn attackChickenRoutine(managed_char: *ManagedCharacter) !void { - const character = managed_char.character; - const action_queue = &managed_char.action_queue; +fn depositIfFull(manager: *Manager, char: *ManagedCharacter) !bool { + const character = char.character; + if (character.getItemCount() < character.inventory_max_items) { + return false; + } + _ = try depositItemsToBank(manager, char); + + if (character.gold > 0) { + try char.action_queue.append(.{ .deposit_gold = @intCast(character.gold) }); + } +} + +fn attackChickenRoutine(manager: *Manager, managed_char: *ManagedCharacter) !void { const chicken_pos = Position{ .x = 0, .y = 1 }; - var character_pos = Position.init(character.x, character.y); - // Deposit items and gold to bank if full - if (try depositIfFull(managed_char)) { + if (try depositIfFull(manager, managed_char)) { return; } // Go to chickens - if (!character_pos.eql(chicken_pos)) { - try action_queue.append(.{ .move = chicken_pos }); + if (try moveIfNeeded(managed_char, chicken_pos)) { return; } // Attack chickens - try action_queue.append(.{ .attack = {} }); + try managed_char.action_queue.append(.{ .attack = {} }); } -fn gatherResourceRoutine(managed_char: *ManagedCharacter, resource_pos: Position) !void { - const character = managed_char.character; - const action_queue = &managed_char.action_queue; - - var character_pos = Position.init(character.x, character.y); - - // Deposit items and gold to bank if full - if (try depositIfFull(managed_char)) { +fn gatherResourceRoutine(manager: *Manager, managed_char: *ManagedCharacter, resource_pos: Position) !void { + if (try depositIfFull(manager, managed_char)) { return; } - if (!character_pos.eql(resource_pos)) { - try action_queue.append(.{ .move = resource_pos }); + if (try moveIfNeeded(managed_char, resource_pos)) { return; } - try action_queue.append(.{ .gather = {} }); + try managed_char.action_queue;.append(.{ .gather = {} }); +} + +fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const ArtifactsAPI.ItemIdQuantity) !bool { + var has_all_items = true; + for (items) |item_quantity| { + const inventory_quantity = char.character.inventory.getItem(item_quantity.id); + if(inventory_quantity < item_quantity.quantity) { + has_all_items = false; + break; + } + } + if (has_all_items) return false; + + if (try moveIfNeeded(char, manager.bank_position)) { + return true; + } + + for (items) |item_quantity| { + const inventory_quantity = char.character.inventory.getItem(item_quantity.id); + if(inventory_quantity < item_quantity.quantity) { + try char.action_queue.append(.{ .withdraw_item = .{ + .id = item_quantity.id, + .quantity = item_quantity.quantity - inventory_quantity, + }}); + } + } + + return true; +} + +fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool { + const inventory_quantity = char.character.inventory.getItem(id); + if (inventory_quantity >= quantity) { + return false; + } + + const code = manager.api.getItemCode(id) orelse return error.InvalidItemId; + const item = try manager.getItem(code) orelse return error.ItemNotFound; + if (item.craft == null) { + return error.NotCraftable; + } + + const recipe = item.craft.?; + const workstation = manager.getWorkstation(recipe.skill); + + if (try moveIfNeeded(char, workstation)) { + return true; + } + + try char.action_queue.append(.{ .craft_item = .{ + .id = id, + .quantity = quantity - inventory_quantity + }}); + + return true; +} + +fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool { + const inventory_quantity = char.character.inventory.getItem(id); + if (inventory_quantity >= quantity) { + return false; + } + + const code = manager.api.getItemCode(id) orelse return error.InvalidItemId; + const target_item = try manager.getItem(code) orelse return error.ItemNotFound; + if (target_item.craft == null) { + return error.NotCraftable; + } + + const recipe = target_item.craft.?; + assert(recipe.quantity == 1); // TODO: Add support for recipe which produce multiple items + + var needed_items = recipe.items; + for (needed_items.slice()) |*needed_item| { + needed_item.quantity *= quantity; + } + + if (try withdrawFromBank(manager, char, needed_items.constSlice())) { + return true; + } + + if (try craftItem(manager, char, id, quantity)) { + return true; + } + + return true; } pub fn main() !void { @@ -208,14 +346,13 @@ pub fn main() !void { std.log.info("Server status: {s} v{s}", .{ status.status, status.version }); std.log.info("Characters online: {}", .{ status.characters_online }); - const characters = try api.listMyCharacters(); + const characters = try api.listMyCharacters(allocator); defer characters.deinit(); - for (characters.items) |character| { - try manager.addCharacter(character); - } - - + // for (characters.list.items) |character| { + // try manager.addCharacter(character); + // } + try manager.addCharacter(characters.list.items[0]); std.log.info("Starting main loop", .{}); while (manager.poll()) |char| { @@ -243,32 +380,56 @@ pub fn main() !void { char.character.x = pos.x; char.character.y = pos.y; }, - .depositGold => |quantity| { + .deposit_gold => |quantity| { std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name}); - var result = try api.actionBankDepositGold(char.character.name, quantity); - defer result.deinit(); + const result = try api.actionBankDepositGold(char.character.name, quantity); cooldown = result.cooldown; char.character.gold -= @intCast(quantity); assert(char.character.gold >= 0); }, - .depositItem => |item| { - std.log.debug("deposit {s}(x{}) from {s}", .{api.getItemCode(item.id).?, item.quantity, char.character.name}); + .deposit_item => |item| { const code = api.getItemCode(item.id) orelse return error.ItemNotFound; - var result = try api.actionBankDepositItem(char.character.name, code, item.quantity); - defer result.deinit(); + std.log.debug("deposit {s} (x{}) from {s}", .{code, item.quantity, char.character.name}); + + const result = try api.actionBankDepositItem(char.character.name, code, item.quantity); cooldown = result.cooldown; char.character.inventory.removeItem(item.id, item.quantity); }, + .withdraw_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + std.log.debug("withdraw {s} (x{}) from {s}", .{code, item.quantity, char.character.name}); + + const result = try api.actionBankWithdrawItem(char.character.name, code, item.quantity); + + cooldown = result.cooldown; + char.character.inventory.addItem(item.id, item.quantity); + }, .gather => { std.log.debug("{s} gathers", .{char.character.name}); var result = try api.actionGather(char.character.name); cooldown = result.cooldown; - for (result.details.items.slice()) |item| { - char.character.inventory.addItem(item.id, @intCast(item.quantity)); + char.character.inventory.addItems(result.details.items.slice()); + }, + .craft_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + std.log.debug("craft {s} (x{}) from {s}", .{code, item.quantity, char.character.name}); + + var result = try api.actionCraft(char.character.name, code, item.quantity); + + cooldown = result.cooldown; + + var inventory = &char.character.inventory; + + const item_details = (try manager.getItem(code)) orelse return error.ItemNotFound; + const recipe = item_details.craft orelse return error.RecipeNotFound; + for (recipe.items.slice()) |recipe_item| { + inventory.removeItem(recipe_item.id, recipe_item.quantity * item.quantity); } + + inventory.addItems(result.details.items.slice()); } } @@ -278,6 +439,16 @@ pub fn main() !void { continue; } + // TODO: Add checking if character state is in sync. Debug mode only + + // if (try craftItemFromBank(&manager, char, try api.getItemId("copper"), 10)) { + // continue; + // } + // + // if (try depositItemsToBank(&manager, char)) { + // continue; + // } + if (std.mem.eql(u8, char.character.name, "Devin")) { try gatherResourceRoutine(char, .{ .x = -1, .y = 0 }); // Ash trees } else if (std.mem.eql(u8, char.character.name, "Dawn")) {