diff --git a/api/errors.zig b/api/errors.zig index d044092..061ec7f 100644 --- a/api/errors.zig +++ b/api/errors.zig @@ -222,6 +222,7 @@ const EquipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ NotFound, CharacterItemAlreadyEquiped, CharacterLocked, + CharacterSlotEquipmentError, CharacterNotSkillLevelRequired, CharacterNotFound, CharacterInCooldown, diff --git a/api/root.zig b/api/root.zig index 665e336..52a2525 100644 --- a/api/root.zig +++ b/api/root.zig @@ -24,6 +24,8 @@ pub const Craft = @import("./schemas/craft.zig"); pub const Resource = @import("./schemas/resource.zig"); pub const MoveResult = @import("./schemas/move_result.zig"); pub const SimpleItem = @import("./schemas/simple_item.zig"); +pub const EquipResult = @import("./schemas/equip_result.zig"); +pub const UnequipResult = EquipResult; const SkillUsageResult = @import("./schemas/skill_usage_result.zig"); pub const GatherResult = SkillUsageResult; pub const CraftResult = SkillUsageResult; diff --git a/api/schemas/equip_result.zig b/api/schemas/equip_result.zig new file mode 100644 index 0000000..e21bdb5 --- /dev/null +++ b/api/schemas/equip_result.zig @@ -0,0 +1,37 @@ +// zig fmt: off +const std = @import("std"); +const json_utils = @import("../json_utils.zig"); +const Store = @import("../store.zig"); +const Character = @import("./character.zig"); +const Cooldown = @import("./cooldown.zig"); +const Item = @import("./item.zig"); + +const EquipResult = @This(); + +pub const Slot = Character.Equipment.SlotId; + +cooldown: Cooldown, +slot: Slot, +item: Item, +character: Character, + +fn parse(store: *Store, obj: std.json.ObjectMap) !EquipResult { + const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; + const item = json_utils.getObject(obj, "item") orelse return error.MissingProperty; + const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; + const slot = try json_utils.getStringRequired(obj, "slot"); + + return EquipResult{ + .character = try Character.parse(store, character), + .cooldown = try Cooldown.parse(store, cooldown), + .item = try Item.parse(store, item), + .slot = Slot.fromString(slot) orelse return error.InvalidSlot + }; +} + +pub fn parseAndUpdate(store: *Store, obj: std.json.ObjectMap) !EquipResult { + const result = try parse(store, obj); + _ = try store.characters.appendOrUpdate(result.character); + _ = try store.items.appendOrUpdate(result.item); + return result; +} diff --git a/api/schemas/equipment.zig b/api/schemas/equipment.zig index 55cf965..1baff1a 100644 --- a/api/schemas/equipment.zig +++ b/api/schemas/equipment.zig @@ -3,27 +3,37 @@ const std = @import("std"); const json_utils = @import("../json_utils.zig"); const Store = @import("../store.zig"); const Item = @import("./item.zig"); +const EnumStringUtils = @import("../enum_string_utils.zig").EnumStringUtils; const Equipment = @This(); -pub const UtilitySlot = struct { - id: Store.Id, - quantity: u64, +pub const Slot = struct { + item: ?Store.Id = null, + quantity: u64 = 0, - fn parse(store: *Store, obj: std.json.ObjectMap, name: []const u8, quantity: []const u8) !?UtilitySlot { + fn parse(store: *Store, obj: std.json.ObjectMap, name: []const u8) !Slot { const item_code = try json_utils.getStringRequired(obj, name); if (item_code.len == 0) { - return null; + return Slot{}; } - return UtilitySlot{ - .id = try store.items.getOrReserveId(item_code), - .quantity = try json_utils.getPositiveIntegerRequired(obj, quantity), + return Slot{ + .item = try store.items.getOrReserveId(item_code), + .quantity = 1 }; } + + fn parseWithQuantity(store: *Store, obj: std.json.ObjectMap, name: []const u8, quantity: []const u8) !Slot { + var slot = try Slot.parse(store, obj, name); + if (slot.item != null) { + slot.quantity = try json_utils.getPositiveIntegerRequired(obj, quantity); + } + + return slot; + } }; -pub const Slot = enum { +pub const SlotId = enum { weapon, shield, helmet, @@ -35,92 +45,56 @@ pub const Slot = enum { amulet, artifact1, artifact2, + artifact3, utility1, utility2, - fn name(self: Slot) []const u8 { - return switch (self) { - .weapon => "weapon", - .shield => "shield", - .helmet => "helmet", - .body_armor => "body_armor", - .leg_armor => "leg_armor", - .boots => "boots", - .ring1 => "ring1", - .ring2 => "ring2", - .amulet => "amulet", - .artifact1 => "artifact1", - .artifact2 => "artifact2", - .utility1 => "utility1", - .utility2 => "utility2", - }; + const Utils = EnumStringUtils(SlotId, .{ + .{ "weapon" , SlotId.weapon }, + .{ "shield" , SlotId.shield }, + .{ "helmet" , SlotId.helmet }, + .{ "body_armor", SlotId.body_armor }, + .{ "leg_armor" , SlotId.leg_armor }, + .{ "boots" , SlotId.boots }, + .{ "ring1" , SlotId.ring1 }, + .{ "ring2" , SlotId.ring2 }, + .{ "amulet" , SlotId.amulet }, + .{ "artifact1" , SlotId.artifact1 }, + .{ "artifact2" , SlotId.artifact2 }, + .{ "artifact3" , SlotId.artifact3 }, + .{ "utility1" , SlotId.utility1 }, + .{ "utility2" , SlotId.utility2 }, + }); + + pub const toString = Utils.toString; + pub const fromString = Utils.fromString; + + pub fn canHoldManyItems(self: SlotId) bool { + return self == .utility1 or self == .utility2; } }; -weapon: ?Store.Id, -shield: ?Store.Id, -helmet: ?Store.Id, -body_armor: ?Store.Id, -leg_armor: ?Store.Id, -boots: ?Store.Id, +pub const Slots = std.EnumArray(SlotId, Slot); -ring1: ?Store.Id, -ring2: ?Store.Id, -amulet: ?Store.Id, - -artifact1: ?Store.Id, -artifact2: ?Store.Id, -artifact3: ?Store.Id, - -utility1: ?UtilitySlot, -utility2: ?UtilitySlot, +slots: Slots, pub fn parse(store: *Store, obj: std.json.ObjectMap) !Equipment { - const weapon = try json_utils.getStringRequired(obj, "weapon_slot"); - const shield = try json_utils.getStringRequired(obj, "shield_slot"); - const helmet = try json_utils.getStringRequired(obj, "helmet_slot"); - const body_armor = try json_utils.getStringRequired(obj, "body_armor_slot"); - const leg_armor = try json_utils.getStringRequired(obj, "leg_armor_slot"); - const boots = try json_utils.getStringRequired(obj, "boots_slot"); - const ring1 = try json_utils.getStringRequired(obj, "ring1_slot"); - const ring2 = try json_utils.getStringRequired(obj, "ring2_slot"); - const amulet = try json_utils.getStringRequired(obj, "amulet_slot"); - const artifact1 = try json_utils.getStringRequired(obj, "artifact1_slot"); - const artifact2 = try json_utils.getStringRequired(obj, "artifact2_slot"); - const artifact3 = try json_utils.getStringRequired(obj, "artifact3_slot"); - return Equipment{ - .weapon = try store.items.getOrReserveId(weapon), - .shield = try store.items.getOrReserveId(shield), - .helmet = try store.items.getOrReserveId(helmet), - .body_armor = try store.items.getOrReserveId(body_armor), - .leg_armor = try store.items.getOrReserveId(leg_armor), - .boots = try store.items.getOrReserveId(boots), - .ring1 = try store.items.getOrReserveId(ring1), - .ring2 = try store.items.getOrReserveId(ring2), - .amulet = try store.items.getOrReserveId(amulet), - .artifact1 = try store.items.getOrReserveId(artifact1), - .artifact2 = try store.items.getOrReserveId(artifact2), - .artifact3 = try store.items.getOrReserveId(artifact3), - .utility1 = try UtilitySlot.parse(store, obj, "utility1_slot", "utility1_slot_quantity"), - .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, + .slots = Slots.init(.{ + .weapon = try Slot.parse(store, obj, "weapon_slot"), + .shield = try Slot.parse(store, obj, "shield_slot"), + .helmet = try Slot.parse(store, obj, "helmet_slot"), + .body_armor = try Slot.parse(store, obj, "body_armor_slot"), + .leg_armor = try Slot.parse(store, obj, "leg_armor_slot"), + .boots = try Slot.parse(store, obj, "boots_slot"), + .ring1 = try Slot.parse(store, obj, "ring1_slot"), + .ring2 = try Slot.parse(store, obj, "ring2_slot"), + .amulet = try Slot.parse(store, obj, "amulet_slot"), + .artifact1 = try Slot.parse(store, obj, "artifact1_slot"), + .artifact2 = try Slot.parse(store, obj, "artifact2_slot"), + .artifact3 = try Slot.parse(store, obj, "artifact3_slot"), + .utility1 = try Slot.parseWithQuantity(store, obj, "utility1_slot", "utility1_slot_quantity"), + .utility2 = try Slot.parseWithQuantity(store, obj, "utility2_slot", "utility2_slot_quantity"), + }) }; } diff --git a/api/server.zig b/api/server.zig index a055ddb..bbb823e 100644 --- a/api/server.zig +++ b/api/server.zig @@ -14,6 +14,7 @@ const AuthToken = Root.AuthToken; const log = std.log.scoped(.api); const ServerURL = std.BoundedArray(u8, 256); +const Equipment = @import("./schemas/equipment.zig"); const Item = @import("./schemas/item.zig"); const Craft = @import("./schemas/craft.zig"); const Status = @import("./schemas/status.zig"); @@ -25,6 +26,8 @@ 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"); +const EquipResult = @import("./schemas/equip_result.zig"); +const UnequipResult = EquipResult; pub const GatherResult = SkillUsageResult; pub const CraftResult = SkillUsageResult; const Image = Store.Image; @@ -502,32 +505,50 @@ fn fetchArray( return result orelse return FetchError.RequestFailed; } -fn prefetch(self: *Server, allocator: std.mem.Allocator) !void { +pub const PrefetchOptions = struct { + resources: bool = true, + maps: bool = true, + monsters: bool = true, + items: bool = true, + images: bool = false, +}; + +pub fn prefetch(self: *Server, allocator: std.mem.Allocator, opts: PrefetchOptions) !void { // TODO: Create a version of `getResources`, `getMonsters`, `getItems`, etc.. // which don't need an allocator to be passed. // This is for cases when you only care that everything will be saved into the store. - const resources = try self.getResources(allocator, .{}); - defer resources.deinit(); + if (opts.resources) { + const resources = try self.getResources(allocator, .{}); + defer resources.deinit(); + } - const maps: std.ArrayList(Map) = try self.getMaps(allocator, .{}); - defer maps.deinit(); + if (opts.maps) { + const maps: std.ArrayList(Map) = try self.getMaps(allocator, .{}); + defer maps.deinit(); + } - const monsters = try self.getMonsters(allocator, .{}); - defer monsters.deinit(); + if (opts.monsters) { + const monsters = try self.getMonsters(allocator, .{}); + defer monsters.deinit(); + } - const items = try self.getItems(allocator, .{}); - defer items.deinit(); + if (opts.items) { + const items = try self.getItems(allocator, .{}); + defer items.deinit(); + } - for (maps.items) |map| { - const skin: []const u8 = map.skin.slice(); - if (self.store.images.getId(.map, skin) == null) { - _ = try self.getImage(.map, skin); + if (opts.images) { + for (self.store.maps.items) |map| { + const skin: []const u8 = map.skin.slice(); + if (self.store.images.getId(.map, skin) == null) { + _ = try self.getImage(.map, skin); + } } } } -pub fn prefetchCached(self: *Server, allocator: std.mem.Allocator, absolute_cache_path: []const u8) !void { +pub fn prefetchCached(self: *Server, allocator: std.mem.Allocator, absolute_cache_path: []const u8, opts: PrefetchOptions) !void { const status: Status = try self.getStatus(); const version = status.version.slice(); @@ -539,7 +560,7 @@ pub fn prefetchCached(self: *Server, allocator: std.mem.Allocator, absolute_cach } else |_| {} } else |_| {} - try self.prefetch(allocator); + try self.prefetch(allocator, opts); const file = try std.fs.createFileAbsolute(absolute_cache_path, .{}); defer file.close(); @@ -1046,6 +1067,66 @@ pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u ); } +pub fn equip(self: *Server, character: []const u8, slot: Equipment.SlotId, item: []const u8, quantity: u64) errors.EquipError!EquipResult { + const path_buff_size = comptime blk: { + var count = 0; + count += 4; // "/my/" + count += Character.max_name_size; + count += 13; // "/action/equip" + break :blk count; + }; + + var path_buff: [path_buff_size]u8 = undefined; + const path = std.fmt.bufPrint( + &path_buff, + "/my/{s}/action/equip",.{ character } + ) catch return FetchError.InvalidPayload; + + var payload_buff: [256]u8 = undefined; + const payload = std.fmt.bufPrint( + &payload_buff, + "{{ \"slot\":\"{s}\", \"code\":\"{s}\", \"quantity\":{} }}", .{ slot.toString(), item, quantity } + ) catch return FetchError.InvalidPayload; + + return try self.fetchObject( + errors.EquipError, + errors.parseEquipError, + EquipResult, + EquipResult.parseAndUpdate, + .{ .method = .POST, .path = path, .payload = payload, .ratelimit = .actions } + ); +} + +pub fn unequip(self: *Server, character: []const u8, slot: Equipment.SlotId, quantity: u64) errors.UnequipError!UnequipResult { + const path_buff_size = comptime blk: { + var count = 0; + count += 4; // "/my/" + count += Character.max_name_size; + count += 15; // "/action/unequip" + break :blk count; + }; + + var path_buff: [path_buff_size]u8 = undefined; + const path = std.fmt.bufPrint( + &path_buff, + "/my/{s}/action/unequip",.{ character } + ) catch return FetchError.InvalidPayload; + + var payload_buff: [256]u8 = undefined; + const payload = std.fmt.bufPrint( + &payload_buff, + "{{ \"slot\":\"{s}\", \"quantity\":{} }}", .{ slot.toString(), quantity } + ) catch return FetchError.InvalidPayload; + + return try self.fetchObject( + errors.UnequipError, + errors.parseUnequipError, + UnequipResult, + UnequipResult.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/cli/main.zig b/cli/main.zig index 9f2f261..5080abb 100644 --- a/cli/main.zig +++ b/cli/main.zig @@ -50,11 +50,10 @@ pub fn main() !void { const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); defer allocator.free(cwd_path); - const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store.bin" }); + const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store-cli.bin" }); defer allocator.free(cache_path); - // TODO: Don't prefetch images - try server.prefetchCached(allocator, cache_path); + try server.prefetchCached(allocator, cache_path, .{ .images = false }); } const character_id = (try server.getCharacter("Blondie")).?; @@ -62,17 +61,10 @@ pub fn main() !void { 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 = 2 + .equip = .{ + .slot = .weapon, + .item = store.items.getId("copper_dagger").? } }); diff --git a/gui/main.zig b/gui/main.zig index c7437ce..cee1bb4 100644 --- a/gui/main.zig +++ b/gui/main.zig @@ -102,10 +102,10 @@ pub fn main() anyerror!void { const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); defer allocator.free(cwd_path); - const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store.bin" }); + const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store-gui.bin" }); defer allocator.free(cache_path); - try server.prefetchCached(allocator, cache_path); + try server.prefetchCached(allocator, cache_path, .{ .images = true }); } rl.initWindow(800, 450, "Artificer"); diff --git a/lib/equip_goal.zig b/lib/equip_goal.zig new file mode 100644 index 0000000..4082842 --- /dev/null +++ b/lib/equip_goal.zig @@ -0,0 +1,61 @@ +// zig fmt: off +const std = @import("std"); +const Api = @import("artifacts-api"); +const Artificer = @import("./root.zig"); + +const Goal = @This(); + +slot: Api.Character.Equipment.SlotId, +item: Api.Store.Id, +quantity: u64 = 1, + +pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) void { + const store = artificer.server.store; + const character = store.characters.get(artificer.character).?; + + const equipment_slot = character.equipment.slots.get(self.slot); + if (equipment_slot.item) |equiped_item|{ + if (equiped_item == self.item and !self.slot.canHoldManyItems()) { + artificer.removeGoal(goal_id); + } else { + artificer.queued_actions.appendAssumeCapacity(.{ + .goal = goal_id, + .action = .{ + .unequip = .{ + .slot = self.slot, + .quantity = self.quantity + } + } + }); + } + return; + } + + artificer.queued_actions.appendAssumeCapacity(.{ + .goal = goal_id, + .action = .{ + .equip = .{ + .slot = self.slot, + .item = self.item, + .quantity = self.quantity + } + } + }); +} + +pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements { + _ = artificer; + + var reqs: Artificer.Requirements = .{}; + reqs.items.addAssumeCapacity(self.item, self.quantity); + + return reqs; +} + +pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer, result: Artificer.ActionResult) void { + _ = self; + + if (result == .equip) { + artificer.removeGoal(goal_id); + } +} diff --git a/lib/root.zig b/lib/root.zig index e8bb45f..84ed827 100644 --- a/lib/root.zig +++ b/lib/root.zig @@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator; const GatherGoal = @import("gather_goal.zig"); const CraftGoal = @import("craft_goal.zig"); +const EquipGoal = @import("equip_goal.zig"); const assert = std.debug.assert; const log = std.log.scoped(.artificer); @@ -31,11 +32,13 @@ const server_down_retry_interval = 5; // minutes pub const Goal = union(enum) { gather: GatherGoal, craft: CraftGoal, + equip: EquipGoal, 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), + .equip => |*equip| equip.tick(goal_id, artificer), } } @@ -43,13 +46,15 @@ pub const Goal = union(enum) { return switch (self) { .gather => |gather| gather.requirements(artificer), .craft => |craft| craft.requirements(artificer), + .equip => |equip| equip.requirements(artificer), }; } - pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void { + pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer, result: Artificer.ActionResult) void { switch (self.*) { .gather => |*gather| gather.onActionCompleted(goal_id, result), .craft => |*craft| craft.onActionCompleted(goal_id, result), + .equip => |*equip| equip.onActionCompleted(goal_id, artificer, result), } } }; @@ -66,13 +71,24 @@ pub const Action = union(enum) { craft: struct { item: Api.Store.Id, quantity: u64 - } + }, + unequip: struct { + slot: Api.Equipment.SlotId, + quantity: u64 + }, + equip: struct { + slot: Api.Equipment.SlotId, + item: Api.Store.Id, + quantity: u64 + }, }; pub const ActionResult = union(enum) { move: Api.MoveResult, gather: Api.GatherResult, craft: Api.CraftResult, + equip: Api.EquipResult, + unequip: Api.UnequipResult, }; const ActionSlot = struct { @@ -81,11 +97,7 @@ const ActionSlot = struct { }; pub const Requirements = struct { - pub const Items = Api.SimpleItem.BoundedArray(blk: { - var max: usize = 0; - max = @max(max, Api.Craft.max_items); - break :blk max; - }); + pub const Items = Api.SimpleItem.BoundedArray(Api.Craft.max_items); items: Items = .{} }; @@ -299,21 +311,28 @@ pub fn tick(self: *Artificer) !void { } const action_slot = self.queued_actions.orderedRemove(0); + const character_name = character.name.slice(); const action_result = switch (action_slot.action) { .move => |position| ActionResult{ - .move = try self.server.move(character.name.slice(), position) + .move = try self.server.move(character_name, position) }, .gather => ActionResult{ - .gather = try self.server.gather(character.name.slice()) + .gather = try self.server.gather(character_name) }, .craft => |craft| ActionResult{ - .craft = try self.server.craft(character.name.slice(), store.items.getName(craft.item).?, craft.quantity) + .craft = try self.server.craft(character_name, store.items.getName(craft.item).?, craft.quantity) + }, + .equip => |equip| ActionResult{ + .equip = try self.server.equip(character_name, equip.slot, store.items.getName(equip.item).?, equip.quantity) + }, + .unequip => |unequip| ActionResult{ + .unequip = try self.server.unequip(character_name, unequip.slot, unequip.quantity) } }; if (self.getGoal(action_slot.goal)) |goal_slot| { const goal = &goal_slot.goal.?; - goal.onActionCompleted(action_slot.goal, action_result); + goal.onActionCompleted(action_slot.goal, self, action_result); } } else { @@ -337,11 +356,24 @@ pub fn tick(self: *Artificer) !void { for (reqs.items.slice()) |req_item| { const inventory_quantity = character.inventory.getQuantity(req_item.id); if (inventory_quantity < req_item.quantity) { + const missing_quantity = req_item.quantity - inventory_quantity; + const item = store.items.get(req_item.id).?; + if (self.findBestResourceWithItem(req_item.id) != null) { const subgoal_id = try self.appendGoal(.{ .gather = .{ .item = req_item.id, - .quantity = req_item.quantity - inventory_quantity, + .quantity = missing_quantity + } + }); + + const subgoal = self.getGoal(subgoal_id).?; + subgoal.parent_goal = goal_id; + } else if (item.craft != null) { + const subgoal_id = try self.appendGoal(.{ + .craft = .{ + .item = req_item.id, + .quantity = missing_quantity } });