From 805e2dcabc0e2468c6e71fa5298b5466eb32fcca Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Wed, 28 Aug 2024 01:20:28 +0300 Subject: [PATCH] refactor main loop --- build.zig | 4 +- src/api/character.zig | 118 ++++++ src/api/combat_stats.zig | 17 + src/api/equipment.zig | 90 +++++ src/api/inventory.zig | 96 +++++ src/{ => api}/json_utils.zig | 0 src/api/position.zig | 15 + src/{artifacts.zig => api/server.zig} | 459 +++++++++++----------- src/api/skill_stats.zig | 17 + src/character.zig | 281 -------------- src/date_time/parse.zig | 42 ++ src/{ => date_time}/timegm.c | 0 src/{ => date_time}/timegm.h | 0 src/main.zig | 532 ++++++++++++-------------- 14 files changed, 892 insertions(+), 779 deletions(-) create mode 100644 src/api/character.zig create mode 100644 src/api/combat_stats.zig create mode 100644 src/api/equipment.zig create mode 100644 src/api/inventory.zig rename src/{ => api}/json_utils.zig (100%) create mode 100644 src/api/position.zig rename src/{artifacts.zig => api/server.zig} (80%) create mode 100644 src/api/skill_stats.zig delete mode 100644 src/character.zig create mode 100644 src/date_time/parse.zig rename src/{ => date_time}/timegm.c (100%) rename src/{ => date_time}/timegm.h (100%) diff --git a/build.zig b/build.zig index e46a5e7..015b346 100644 --- a/build.zig +++ b/build.zig @@ -11,8 +11,8 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); exe.linkLibC(); - exe.addIncludePath(b.path("src")); - exe.addCSourceFile(.{ .file = b.path("src/timegm.c") }); + exe.addIncludePath(b.path("src/date_time")); + exe.addCSourceFile(.{ .file = b.path("src/date_time/timegm.c") }); b.installArtifact(exe); diff --git a/src/api/character.zig b/src/api/character.zig new file mode 100644 index 0000000..0f74339 --- /dev/null +++ b/src/api/character.zig @@ -0,0 +1,118 @@ +const std = @import("std"); +const json_utils = @import("json_utils.zig"); +const Server = @import("./server.zig"); +const Position = @import("./position.zig"); +const parseDateTime = Server.parseDateTime; +const ItemId = Server.ItemId; +const Allocator = std.mem.Allocator; +const json = std.json; +const assert = std.debug.assert; + +const SkillStats = @import("./skill_stats.zig"); +const CombatStats = @import("./combat_stats.zig"); +const Equipment = @import("./equipment.zig"); +const Inventory = @import("./inventory.zig"); + +const Character = @This(); + +allocator: Allocator, + +name: []u8, +skin: []u8, +account: ?[]u8, +gold: i64, +hp: i64, +haste: i64, +position: Position, +cooldown_expiration: f64, + +combat: SkillStats, +mining: SkillStats, +woodcutting: SkillStats, +fishing: SkillStats, +weaponcrafting: SkillStats, +gearcrafting: SkillStats, +jewelrycrafting: SkillStats, +cooking: SkillStats, + +water: CombatStats, +fire: CombatStats, +earth: CombatStats, +air: CombatStats, + +equipment: Equipment, + +inventory_max_items: i64, +inventory: Inventory, + +pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character { + const inventory = json_utils.getArray(obj, "inventory") orelse return error.MissingProperty; + const cooldown_expiration = json_utils.getString(obj, "cooldown_expiration") orelse return error.MissingProperty; + + const x = try json_utils.getIntegerRequired(obj, "x"); + const y = try json_utils.getIntegerRequired(obj, "y"); + const name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty; + assert(name.len > 0); + + return Character{ + .allocator = allocator, + .account = try json_utils.dupeString(allocator, obj, "account"), + .name = name, + .skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty, + + .gold = try json_utils.getIntegerRequired(obj, "gold"), + .hp = try json_utils.getIntegerRequired(obj, "hp"), + .haste = try json_utils.getIntegerRequired(obj, "haste"), + .position = Position.init(x, y), + .cooldown_expiration = parseDateTime(cooldown_expiration) orelse return error.InvalidDateTime, + + .combat = try SkillStats.parse(obj, "level", "xp", "max_xp"), + .mining = try SkillStats.parse(obj, "mining_level", "mining_xp", "mining_max_xp"), + .woodcutting = try SkillStats.parse(obj, "woodcutting_level", "woodcutting_xp", "woodcutting_max_xp"), + .fishing = try SkillStats.parse(obj, "fishing_level", "fishing_xp", "fishing_max_xp"), + .weaponcrafting = try SkillStats.parse(obj, "weaponcrafting_level", "weaponcrafting_xp", "weaponcrafting_max_xp"), + .gearcrafting = try SkillStats.parse(obj, "gearcrafting_level", "gearcrafting_xp", "gearcrafting_max_xp"), + .jewelrycrafting = try SkillStats.parse(obj, "jewelrycrafting_level", "jewelrycrafting_xp", "jewelrycrafting_max_xp"), + .cooking = try SkillStats.parse(obj, "cooking_level", "cooking_xp", "cooking_max_xp"), + + .water = try CombatStats.parse(obj, "attack_water", "dmg_water", "res_water"), + .fire = try CombatStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"), + .earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"), + .air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"), + + .equipment = try Equipment.parse(api, obj), + + .inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty, + .inventory = try Inventory.parse(api, inventory) + }; +} + +pub fn deinit(self: *Character) void { + if (self.account) |str| self.allocator.free(str); + self.allocator.free(self.name); + self.allocator.free(self.skin); +} + +pub fn getItemCount(self: *const Character) u64 { + var count: u64 = 0; + for (self.inventory.slots) |slot| { + count += slot.quantity; + } + return count; +} + +pub fn format( + self: Character, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = fmt; + _ = options; + + try writer.print("{s}{{ .name = \"{s}\", .position = {} ... }}", .{ + @typeName(Character), + self.name, + self.position + }); +} diff --git a/src/api/combat_stats.zig b/src/api/combat_stats.zig new file mode 100644 index 0000000..22d7a9f --- /dev/null +++ b/src/api/combat_stats.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const json_utils = @import("json_utils.zig"); +const json = std.json; + +const CombatStats = @This(); + +attack: i64, +damage: i64, +resistance: i64, + +pub fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats { + return CombatStats{ + .attack = try json_utils.getIntegerRequired(object, attack), + .damage = try json_utils.getIntegerRequired(object, damage), + .resistance = try json_utils.getIntegerRequired(object, resistance), + }; +} diff --git a/src/api/equipment.zig b/src/api/equipment.zig new file mode 100644 index 0000000..19f9aa4 --- /dev/null +++ b/src/api/equipment.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const json_utils = @import("json_utils.zig"); +const Server = @import("./server.zig"); +const ItemId = Server.ItemId; +const json = std.json; + +const Equipment = @This(); + +pub const Consumable = struct { + id: ?ItemId, + quantity: i64, + + fn parse(api: *Server, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable { + return Consumable{ + .id = try api.getItemIdJson(obj, name), + .quantity = try json_utils.getIntegerRequired(obj, quantity), + }; + } +}; + +pub const Slot = enum { + weapon, + shield, + helmet, + body_armor, + leg_armor, + boots, + ring1, + ring2, + amulet, + artifact1, + artifact2, + consumable1, + consumable2, + + 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", + .consumable1 => "consumable1", + .consumable2 => "consumable2", + }; + } +}; + +weapon: ?ItemId, +shield: ?ItemId, +helmet: ?ItemId, +body_armor: ?ItemId, +leg_armor: ?ItemId, +boots: ?ItemId, + +ring1: ?ItemId, +ring2: ?ItemId, +amulet: ?ItemId, + +artifact1: ?ItemId, +artifact2: ?ItemId, +artifact3: ?ItemId, + +consumable1: Consumable, +consumable2: Consumable, + +pub fn parse(api: *Server, obj: json.ObjectMap) !Equipment { + return Equipment{ + .weapon = try api.getItemIdJson(obj, "weapon_slot"), + .shield = try api.getItemIdJson(obj, "shield_slot"), + .helmet = try api.getItemIdJson(obj, "helmet_slot"), + .body_armor = try api.getItemIdJson(obj, "body_armor_slot"), + .leg_armor = try api.getItemIdJson(obj, "leg_armor_slot"), + .boots = try api.getItemIdJson(obj, "boots_slot"), + .ring1 = try api.getItemIdJson(obj, "ring1_slot"), + .ring2 = try api.getItemIdJson(obj, "ring2_slot"), + .amulet = try api.getItemIdJson(obj, "amulet_slot"), + .artifact1 = try api.getItemIdJson(obj, "artifact1_slot"), + .artifact2 = try api.getItemIdJson(obj, "artifact2_slot"), + .artifact3 = try api.getItemIdJson(obj, "artifact3_slot"), + .consumable1 = try Consumable.parse(api, obj, "consumable1_slot", "consumable1_slot_quantity"), + .consumable2 = try Consumable.parse(api, obj, "consumable2_slot", "consumable2_slot_quantity"), + }; +} diff --git a/src/api/inventory.zig b/src/api/inventory.zig new file mode 100644 index 0000000..29468a0 --- /dev/null +++ b/src/api/inventory.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const json_utils = @import("json_utils.zig"); +const Server = @import("./server.zig"); +const ItemId = Server.ItemId; +const assert = std.debug.assert; +const json = std.json; + +const Inventory = @This(); + +const slot_count = 20; + +pub const Slot = struct { + id: ?ItemId, + quantity: u64, + + fn parse(api: *Server, slot_obj: json.ObjectMap) !Slot { + const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity"); + if (quantity < 0) return error.InvalidQuantity; + + return Slot{ + .id = try api.getItemIdJson(slot_obj, "code"), + .quantity = @intCast(quantity), + }; + } +}; + +slots: [slot_count]Slot, + +pub fn parse(api: *Server, slots_array: json.Array) !Inventory { + assert(slots_array.items.len == Inventory.slot_count); + + var inventory: Inventory = undefined; + + for (0.., slots_array.items) |i, slot_value| { + const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType; + inventory.slots[i] = try Slot.parse(api, slot_obj); + } + + return inventory; +} + +fn findSlot(self: *Inventory, id: ItemId) ?*Slot { + for (&self.slots) |*slot| { + if (slot.id == id) { + return slot; + } + } + return null; +} + +pub fn removeItem(self: *Inventory, id: ItemId, quantity: u64) void { + const slot = self.findSlot(id) orelse unreachable; + assert(slot.quantity >= quantity); + + slot.quantity -= quantity; + if (slot.quantity == 0) { + slot.id = null; + } +} + +pub fn addItem(self: *Inventory, id: ItemId, quantity: u64) void { + if (self.findSlot(id)) |slot| { + slot.quantity += quantity; + } else { + var empty_slot: ?*Slot = null; + for (&self.slots) |*slot| { + if (slot.id == null) { + empty_slot = slot; + } + } + + assert(empty_slot != null); + empty_slot.?.id = id; + empty_slot.?.quantity = quantity; + } +} + +pub fn addItems(self: *Inventory, items: []const Server.ItemIdQuantity) void { + for (items) |item| { + self.addItem(item.id, item.quantity); + } +} + +pub fn removeItems(self: *Inventory, items: []const Server.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; +} diff --git a/src/json_utils.zig b/src/api/json_utils.zig similarity index 100% rename from src/json_utils.zig rename to src/api/json_utils.zig diff --git a/src/api/position.zig b/src/api/position.zig new file mode 100644 index 0000000..f8cf052 --- /dev/null +++ b/src/api/position.zig @@ -0,0 +1,15 @@ +const Position = @This(); + +x: i64, +y: i64, + +pub fn init(x: i64, y: i64) Position { + return Position{ + .x = x, + .y = y + }; +} + +pub fn eql(self: Position, other: Position) bool { + return self.x == other.x and self.y == other.y; +} diff --git a/src/artifacts.zig b/src/api/server.zig similarity index 80% rename from src/artifacts.zig rename to src/api/server.zig index 4416634..6eaba07 100644 --- a/src/artifacts.zig +++ b/src/api/server.zig @@ -3,13 +3,17 @@ const assert = std.debug.assert; const json = std.json; const Allocator = std.mem.Allocator; +// TODO: Maybe it would be good to move date time parsing to separate module +pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime; + const json_utils = @import("json_utils.zig"); pub const Character = @import("character.zig"); // Specification: https://api.artifactsmmo.com/docs -const ArtifactsAPI = @This(); +const Server = @This(); +const log = std.log.scoped(.api); pub const ItemId = u32; allocator: Allocator, @@ -21,6 +25,8 @@ server_uri: std.Uri, token: ?[]u8 = null, item_codes: std.ArrayList([]u8), +characters: std.ArrayList(Character), +items: std.StringHashMap(Item), // ------------------------- API errors ------------------------ @@ -126,39 +132,7 @@ pub const EquipError = APIError || error { // ------------------------- API result structs ------------------------ -pub const EquipmentSlot = enum { - weapon, - shield, - helmet, - body_armor, - leg_armor, - boots, - ring1, - ring2, - amulet, - artifact1, - artifact2, - consumable1, - consumable2, - - fn name(self: EquipmentSlot) []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", - .consumable1 => "consumable1", - .consumable2 => "consumable2", - }; - } -}; +pub const EquipmentSlot = @import("./equipment.zig").Slot; pub const Skill = enum { weaponcrafting, @@ -198,7 +172,7 @@ const ServerStatus = struct { version: []const u8, characters_online: i64, - pub fn parse(api: *ArtifactsAPI, object: json.ObjectMap, allocator: Allocator) !ServerStatus { + pub fn parse(api: *Server, object: json.ObjectMap, allocator: Allocator) !ServerStatus { _ = api; return ServerStatus{ @@ -215,55 +189,6 @@ const ServerStatus = struct { } }; -pub fn parseDateTime(datetime: []const u8) ?f64 { - const time_h = @cImport({ - @cDefine("_XOPEN_SOURCE", "700"); - @cInclude("stddef.h"); - @cInclude("stdio.h"); - @cInclude("time.h"); - @cInclude("timegm.h"); - }); - - var buffer: [256]u8 = undefined; - var fba = std.heap.FixedBufferAllocator.init(&buffer); - var allocator = fba.allocator(); - - const datetime_z = allocator.dupeZ(u8, datetime) catch return null; - - var tm: time_h.tm = .{}; - const s = time_h.strptime(datetime_z, "%Y-%m-%dT%H:%M:%S.", &tm); - if (s == null) { - return null; - } - - const s_len = std.mem.len(s); - if (s[s_len-1] != 'Z') { - return null; - } - - const milliseconds_str = s[0..(s_len-1)]; - var milliseconds: f64 = @floatFromInt(std.fmt.parseUnsigned(u32, milliseconds_str, 10) catch return null); - while (milliseconds >= 1) { - milliseconds /= 10; - } - - const seconds: f64 = @floatFromInt(time_h.my_timegm(&tm)); - - return seconds + milliseconds; -} - -pub const CharacterList = struct { - allocator: Allocator, - items: []Character, - - pub fn deinit(self: CharacterList) void { - for (self.items) |*char| { - char.deinit(); - } - self.allocator.free(self.items); - } -}; - pub const Cooldown = struct { pub const Reason = enum { movement, @@ -339,7 +264,7 @@ pub const FightResult = struct { drops: std.BoundedArray(DroppedItem, 8), result: Result, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Details { + fn parse(api: *Server, obj: json.ObjectMap) !Details { const result = try json_utils.getStringRequired(obj, "result"); var result_enum: Result = undefined; if (std.mem.eql(u8, result, "win")) { @@ -371,14 +296,17 @@ pub const FightResult = struct { cooldown: Cooldown, fight: Details, + character: Character, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !FightResult { + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !FightResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const fight = json_utils.getObject(obj, "fight") orelse return error.MissingProperty; + const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return FightResult{ .cooldown = try Cooldown.parse(cooldown), - .fight = try Details.parse(api, fight) + .fight = try Details.parse(api, fight), + .character = try Character.parse(api, character, allocator) }; } @@ -398,7 +326,7 @@ pub const ItemIdQuantity = struct { id: ItemId, quantity: u64, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemIdQuantity { + pub fn parse(api: *Server, 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; @@ -411,7 +339,7 @@ pub const ItemIdQuantity = struct { }; const BoundedItems = std.BoundedArray(ItemIdQuantity, 8); -fn parseSimpleItemList(api: *ArtifactsAPI, array: json.Array) !BoundedItems { +fn parseSimpleItemList(api: *Server, array: json.Array) !BoundedItems { var items = BoundedItems.init(0) catch unreachable; for (array.items) |item_value| { @@ -427,7 +355,7 @@ pub const SkillResultDetails = struct { xp: i64, items: BoundedItems, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !SkillResultDetails { + fn parse(api: *Server, obj: json.ObjectMap) !SkillResultDetails { const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty; return SkillResultDetails{ @@ -440,14 +368,17 @@ pub const SkillResultDetails = struct { pub const GatherResult = struct { cooldown: Cooldown, details: SkillResultDetails, + character: Character, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GatherResult { + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !GatherResult { 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 GatherResult{ .cooldown = try Cooldown.parse(cooldown), - .details = try SkillResultDetails.parse(api, details) + .details = try SkillResultDetails.parse(api, details), + .character = try Character.parse(api, character, allocator) }; } @@ -466,14 +397,15 @@ pub const GatherResult = struct { pub const MoveResult = struct { cooldown: Cooldown, + character: Character, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult { - _ = api; - + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !MoveResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; + const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return MoveResult{ - .cooldown = try Cooldown.parse(cooldown) + .cooldown = try Cooldown.parse(cooldown), + .character = try Character.parse(api, character, allocator) }; } @@ -487,21 +419,19 @@ pub const MoveResult = struct { else => null }; } - - pub fn deinit(self: MoveResult) void { - _ = self; - } }; pub const GoldTransactionResult = struct { cooldown: Cooldown, + character: Character, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult { - _ = api; + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !GoldTransactionResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; + const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return GoldTransactionResult{ - .cooldown = try Cooldown.parse(cooldown) + .cooldown = try Cooldown.parse(cooldown), + .character = try Character.parse(api, character, allocator) }; } @@ -532,13 +462,15 @@ pub const GoldTransactionResult = struct { pub const ItemTransactionResult = struct { cooldown: Cooldown, + character: Character, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult { - _ = api; + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !ItemTransactionResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; + const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; return ItemTransactionResult{ - .cooldown = try Cooldown.parse(cooldown) + .cooldown = try Cooldown.parse(cooldown), + .character = try Character.parse(api, character, allocator) }; } @@ -573,14 +505,17 @@ pub const ItemTransactionResult = struct { pub const CraftResult = struct { cooldown: Cooldown, details: SkillResultDetails, + character: Character, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !CraftResult { + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !CraftResult { 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 CraftResult{ .cooldown = try Cooldown.parse(cooldown), - .details = try SkillResultDetails.parse(api, details) + .details = try SkillResultDetails.parse(api, details), + .character = try Character.parse(api, character, allocator) }; } @@ -603,7 +538,7 @@ pub const UnequipResult = struct { cooldown: Cooldown, item: ItemId, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !UnequipResult { + pub fn parse(api: *Server, obj: json.ObjectMap) !UnequipResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const item = json_utils.getObject(obj, "item") orelse return error.MissingProperty; @@ -635,7 +570,7 @@ pub const UnequipResult = struct { pub const EquipResult = struct { cooldown: Cooldown, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !EquipResult { + pub fn parse(api: *Server, obj: json.ObjectMap) !EquipResult { _ = api; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; @@ -673,7 +608,7 @@ pub const MapResult = struct { y: i64, content: ?MapContent, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !MapResult { + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !MapResult { _ = api; var content: ?MapContent = null; @@ -710,7 +645,7 @@ pub const Item = struct { quantity: u64, items: BoundedItems, - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Recipe { + pub fn parse(api: *Server, 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; @@ -740,7 +675,7 @@ pub const Item = struct { // TODO: effects // TODO: Grand exchange - pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !Item { + pub fn parse(api: *Server, 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; @@ -781,20 +716,23 @@ pub const ArtifactsFetchResult = struct { // ------------------------- General API methods ------------------------ -pub fn init(allocator: Allocator) !ArtifactsAPI { - const server = try allocator.dupe(u8, "https://api.artifactsmmo.com"); - const server_uri = std.Uri.parse(server) catch unreachable; +pub fn init(allocator: Allocator) !Server { + const url = try allocator.dupe(u8, "https://api.artifactsmmo.com"); + const uri = std.Uri.parse(url) catch unreachable; - return ArtifactsAPI{ + return Server{ .allocator = allocator, - .item_codes = std.ArrayList([]u8).init(allocator), .client = .{ .allocator = allocator }, - .server = server, - .server_uri = server_uri + .server = url, + .server_uri = uri, + + .item_codes = std.ArrayList([]u8).init(allocator), + .characters = std.ArrayList(Character).init(allocator), + .items = std.StringHashMap(Item).init(allocator), }; } -pub fn deinit(self: *ArtifactsAPI) void { +pub fn deinit(self: *Server) void { self.client.deinit(); self.allocator.free(self.server); if (self.token) |str| self.allocator.free(str); @@ -803,6 +741,17 @@ pub fn deinit(self: *ArtifactsAPI) void { self.allocator.free(code); } self.item_codes.deinit(); + + for (self.characters.items) |*char| { + char.deinit(); + } + self.characters.deinit(); + + var itemsIter = self.items.valueIterator(); + while (itemsIter.next()) |item| { + item.deinit(); + } + self.items.deinit(); } const FetchOptions = struct { @@ -825,7 +774,7 @@ fn allocPaginationParams(allocator: Allocator, page: u64, page_size: ?u64) ![]u8 } } -fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResult { +fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult { const method = options.method; const path = options.path; const payload = options.payload; @@ -879,11 +828,11 @@ fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResu opts.headers.authorization = .{ .override = authorization_header.? }; } - std.log.debug("fetch {} {s}", .{method, path}); + log.debug("fetch {} {s}", .{method, path}); const result = self.client.fetch(opts) catch return APIError.RequestFailed; const response_body = response_storage.items; - std.log.debug("fetch result {}", .{result.status}); + log.debug("fetch result {}", .{result.status}); if (result.status == .service_unavailable) { return APIError.ServerUnavailable; @@ -952,7 +901,7 @@ fn handleFetchError( } fn fetchOptionalObject( - self: *ArtifactsAPI, + self: *Server, Error: type, parseError: ?fn (status: std.http.Status) ?Error, Object: type, @@ -986,7 +935,7 @@ fn fetchOptionalObject( } fn fetchObject( - self: *ArtifactsAPI, + self: *Server, Error: type, parseError: ?fn (status: std.http.Status) ?Error, Object: type, @@ -998,27 +947,8 @@ fn fetchObject( return result orelse return APIError.RequestFailed; } -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, + self: *Server, allocator: Allocator, Error: type, parseError: ?fn (status: std.http.Status) ?Error, @@ -1026,7 +956,7 @@ fn fetchOptionalArray( parseObject: anytype, parseObjectArgs: anytype, fetchOptions: FetchOptions -) Error!?ObjectList(Object) { +) Error!?std.ArrayList(Object) { if (@typeInfo(@TypeOf(parseObject)) != .Fn) { @compileError("`parseObject` must be a function"); } @@ -1048,35 +978,29 @@ fn fetchOptionalArray( return APIError.ParseFailed; } - 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(); - // } + var array = std.ArrayList(Object).init(allocator); + errdefer { + if (std.meta.hasFn(Object, "deinit")) { + for (array.items) |*item| { + 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; - object_array.list.append(parsed_item) catch return APIError.OutOfMemory; + array.append(parsed_item) catch return APIError.OutOfMemory; } - return object_array; + return array; } fn fetchArray( - self: *ArtifactsAPI, + self: *Server, allocator: Allocator, Error: type, parseError: ?fn (status: std.http.Status) ?Error, @@ -1084,12 +1008,12 @@ fn fetchArray( parseObject: anytype, parseObjectArgs: anytype, fetchOptions: FetchOptions -) Error!ObjectList(Object) { +) Error!std.ArrayList(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 { +pub fn setURL(self: *Server, url: []const u8) !void { const url_dupe = self.allocator.dupe(u8, url); errdefer self.allocator.free(url_dupe); @@ -1100,7 +1024,7 @@ pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void { self.server_uri = uri; } -pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void { +pub fn setToken(self: *Server, token: ?[]const u8) !void { var new_token: ?[]u8 = null; if (token != null) { new_token = try self.allocator.dupe(u8, token.?); @@ -1110,7 +1034,7 @@ pub fn setToken(self: *ArtifactsAPI, token: ?[]const u8) !void { self.token = new_token; } -pub fn getItemId(self: *ArtifactsAPI, code: []const u8) !ItemId { +pub fn getItemId(self: *Server, code: []const u8) !ItemId { assert(code.len != 0); for (0.., self.item_codes.items) |i, item_code| { @@ -1126,7 +1050,7 @@ pub fn getItemId(self: *ArtifactsAPI, code: []const u8) !ItemId { return @intCast(self.item_codes.items.len - 1); } -pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 { +pub fn getItemCode(self: *const Server, id: ItemId) ?[]const u8 { if (id >= self.item_codes.items.len) { return null; } @@ -1134,9 +1058,49 @@ pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 { return self.item_codes.items[id]; } +pub fn getItemIdJson(self: *Server, object: json.ObjectMap, name: []const u8) !?ItemId { + const code = try json_utils.getStringRequired(object, name); + if (code.len == 0) { + return null; + } + + return try self.getItemId(code); +} + +fn findCharacterIndex(self: *const Server, name: []const u8) ?usize { + for (0.., self.characters.items) |i, character| { + if (std.mem.eql(u8, character.name, name)) { + return i; + } + } + + return null; +} + +fn addOrUpdateCharacter(self: *Server, character: Character) !void { + if (self.findCharacterIndex(character.name)) |found| { + self.characters.items[found].deinit(); + self.characters.items[found] = character; + } else { + try self.characters.append(character); + } +} + +pub fn findCharacter(self: *const Server, name: []const u8) ?Character { + if (self.findCharacterIndex(name)) |index| { + return self.characters.items[index]; + } + + return null; +} + +pub fn findItem(self: *const Server, name: []const u8) ?Item { + return self.items.get(name); +} + // ------------------------- Endpoints ------------------------ -pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus { +pub fn getServerStatus(self: *Server) !ServerStatus { return try self.fetchObject( APIError, null, @@ -1146,74 +1110,97 @@ pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus { ); } -pub fn getCharacter(self: *ArtifactsAPI, allocator: Allocator, name: []const u8) APIError!?Character { +pub fn getCharacter(self: *Server, name: []const u8) APIError!?Character { const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name}); defer self.allocator.free(path); - return try self.fetchOptionalObject( + var maybe_character = try self.fetchOptionalObject( APIError, null, Character, - Character.parse, .{ allocator }, + Character.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); + + if (maybe_character) |*character| { + errdefer character.deinit(); + try self.addOrUpdateCharacter(character.*); + return character.*; + } else { + return null; + } } -pub fn listMyCharacters(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Character) { - return self.fetchArray( - allocator, +pub fn listMyCharacters(self: *Server) APIError!std.ArrayList(Character) { + const characters = try self.fetchArray( + self.allocator, APIError, null, Character, - Character.parse, .{ allocator }, + Character.parse, .{ self.allocator }, .{ .method = .GET, .path = "/my/characters" } ); + errdefer characters.deinit(); + for (characters.items) |character| { + try self.addOrUpdateCharacter(character); + } + + return characters; } -pub fn actionFight(self: *ArtifactsAPI, name: []const u8) FightError!FightResult { +pub fn actionFight(self: *Server, name: []const u8) FightError!FightResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name}); defer self.allocator.free(path); - return try self.fetchObject( + const result = try self.fetchObject( FightError, FightResult.parseError, FightResult, - FightResult.parse, .{ }, + FightResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } ); + try self.addOrUpdateCharacter(result.character); + + return result; } -pub fn actionGather(self: *ArtifactsAPI, name: []const u8) GatherError!GatherResult { +pub fn actionGather(self: *Server, name: []const u8) GatherError!GatherResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/gathering", .{name}); defer self.allocator.free(path); - return try self.fetchObject( + const result = try self.fetchObject( GatherError, GatherResult.parseError, GatherResult, - GatherResult.parse, .{ }, + GatherResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } ); + try self.addOrUpdateCharacter(result.character); + + return result; } -pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) MoveError!MoveResult { +pub fn actionMove(self: *Server, name: []const u8, x: i64, y: i64) MoveError!MoveResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/move", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( MoveError, MoveResult.parseError, MoveResult, - MoveResult.parse, .{ }, + MoveResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } pub fn actionBankDepositGold( - self: *ArtifactsAPI, + self: *Server, name: []const u8, quantity: u64 ) BankDepositGoldError!GoldTransactionResult { @@ -1223,17 +1210,20 @@ pub fn actionBankDepositGold( const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( BankDepositGoldError, GoldTransactionResult.parseDepositError, GoldTransactionResult, - GoldTransactionResult.parse, .{ }, + GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } pub fn actionBankDepositItem( - self: *ArtifactsAPI, + self: *Server, name: []const u8, code: []const u8, quantity: u64 @@ -1244,17 +1234,20 @@ pub fn actionBankDepositItem( const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( BankDepositItemError, ItemTransactionResult.parseDepositError, ItemTransactionResult, - ItemTransactionResult.parse, .{ }, + ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } pub fn actionBankWithdrawGold( - self: *ArtifactsAPI, + self: *Server, name: []const u8, quantity: u64 ) BankDepositGoldError!GoldTransactionResult { @@ -1264,17 +1257,20 @@ pub fn actionBankWithdrawGold( const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( BankWithdrawGoldError, GoldTransactionResult.parseWithdrawError, GoldTransactionResult, - GoldTransactionResult.parse, .{ }, + GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } pub fn actionBankWithdrawItem( - self: *ArtifactsAPI, + self: *Server, name: []const u8, code: []const u8, quantity: u64 @@ -1285,17 +1281,20 @@ pub fn actionBankWithdrawItem( const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( BankWithdrawItemError, ItemTransactionResult.parseWithdrawError, ItemTransactionResult, - ItemTransactionResult.parse, .{ }, + ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } pub fn actionCraft( - self: *ArtifactsAPI, + self: *Server, name: []const u8, code: []const u8, quantity: u64 @@ -1306,17 +1305,20 @@ pub fn actionCraft( const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( CraftError, CraftResult.parseError, CraftResult, - CraftResult.parse, .{ }, + CraftResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } pub fn actionUnequip( - self: *ArtifactsAPI, + self: *Server, name: []const u8, slot: EquipmentSlot ) UnequipError!UnequipResult { @@ -1326,17 +1328,20 @@ pub fn actionUnequip( const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\" }}", .{slot.name()}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( UnequipError, UnequipResult.parseError, UnequipResult, - UnequipResult.parse, .{ }, + UnequipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } pub fn actionEquip( - self: *ArtifactsAPI, + self: *Server, name: []const u8, slot: EquipmentSlot, code: []const u8 @@ -1347,16 +1352,19 @@ pub fn actionEquip( const payload = try std.fmt.allocPrint(self.allocator, "{{ \"slot\":\"{s}\",\"code\":\"{s}\" }}", .{slot.name(), code}); defer self.allocator.free(payload); - return try self.fetchObject( + const result = try self.fetchObject( EquipError, EquipResult.parseError, EquipResult, - EquipResult.parse, .{ }, + EquipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } ); + try self.addOrUpdateCharacter(result.character); + + return result; } -pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 { +pub fn getBankGold(self: *Server) APIError!u64 { const result = try self.fetch(.{ .method = .GET, .path = "/my/bank/gold" }); defer result.deinit(); @@ -1374,7 +1382,7 @@ pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 { return @intCast(quantity); } -pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(ItemIdQuantity) { +pub fn getBankItems(self: *Server, allocator: Allocator) APIError!std.ArrayList(ItemIdQuantity) { return self.fetchArray( allocator, APIError, @@ -1385,7 +1393,7 @@ pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectLi ); } -pub fn getBankItemQuantity(self: *ArtifactsAPI, code: []const u8) APIError!?u64 { +pub fn getBankItemQuantity(self: *Server, code: []const u8) APIError!?u64 { const query = try std.fmt.allocPrint(self.allocator, "item_code={s}", .{code}); defer self.allocator.free(query); @@ -1411,7 +1419,7 @@ pub fn getBankItemQuantity(self: *ArtifactsAPI, code: []const u8) APIError!?u64 return list_items[0].quantity; } -pub fn getMap(self: *ArtifactsAPI, allocator: Allocator, x: i64, y: i64) APIError!?MapResult { +pub fn getMap(self: *Server, allocator: Allocator, x: i64, y: i64) APIError!?MapResult { const path = try std.fmt.allocPrint(self.allocator, "/maps/{}/{}", .{x, y}); defer self.allocator.free(path); @@ -1424,7 +1432,7 @@ pub fn getMap(self: *ArtifactsAPI, allocator: Allocator, x: i64, y: i64) APIErro ); } -pub fn getMaps(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(MapResult) { +pub fn getMaps(self: *Server, allocator: Allocator) APIError!std.ArrayList(MapResult) { return self.fetchArray( allocator, APIError, @@ -1435,19 +1443,32 @@ pub fn getMaps(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Ma ); } -pub fn getItem(self: *ArtifactsAPI, allocator: Allocator, code: []const u8) APIError!?Item { +pub fn getItem(self: *Server, code: []const u8) APIError!?Item { + if (self.findItem(code)) |item| { + return item; + } + const path = try std.fmt.allocPrint(self.allocator, "/items/{s}", .{code}); defer self.allocator.free(path); - return self.fetchOptionalObject( + const result = try self.fetchOptionalObject( APIError, null, Item, - Item.parse, .{ allocator }, + Item.parse, .{ self.allocator }, .{ .method = .GET, .path = path } ); -} -test "parse date time" { - try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?); + if (result) |item| { + const item_id = try self.getItemId(code); + const code_owned = self.getItemCode(item_id).?; + + var entry = try self.items.getOrPut(code_owned); + if (entry.found_existing) { + entry.value_ptr.deinit(); + } + entry.value_ptr.* = item; + } + + return result; } diff --git a/src/api/skill_stats.zig b/src/api/skill_stats.zig new file mode 100644 index 0000000..4ecdb59 --- /dev/null +++ b/src/api/skill_stats.zig @@ -0,0 +1,17 @@ +const std = @import("std"); +const json_utils = @import("./json_utils.zig"); +const json = std.json; + +const SkillStats = @This(); + +level: i64, +xp: i64, +max_xp: i64, + +pub fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats { + return SkillStats{ + .level = try json_utils.getIntegerRequired(object, level), + .xp = try json_utils.getIntegerRequired(object, xp), + .max_xp = try json_utils.getIntegerRequired(object, max_xp), + }; +} diff --git a/src/character.zig b/src/character.zig deleted file mode 100644 index 1744595..0000000 --- a/src/character.zig +++ /dev/null @@ -1,281 +0,0 @@ -const std = @import("std"); -const json_utils = @import("json_utils.zig"); -const ArtifactsAPI = @import("artifacts.zig"); -const parseDateTime = ArtifactsAPI.parseDateTime; -const ItemId = ArtifactsAPI.ItemId; -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const json = std.json; - -const Character = @This(); - -fn getItemId(api: *ArtifactsAPI, object: json.ObjectMap, name: []const u8) !?ItemId { - const code = try json_utils.getStringRequired(object, name); - if (code.len == 0) { - return null; - } - - return try api.getItemId(code); -} - -pub const SkillStats = struct { - level: i64, - xp: i64, - max_xp: i64, - - fn parse(object: json.ObjectMap, level: []const u8, xp: []const u8, max_xp: []const u8) !SkillStats { - return SkillStats{ - .level = try json_utils.getIntegerRequired(object, level), - .xp = try json_utils.getIntegerRequired(object, xp), - .max_xp = try json_utils.getIntegerRequired(object, max_xp), - }; - } -}; - -pub const CombatStats = struct { - attack: i64, - damage: i64, - resistance: i64, - - fn parse(object: json.ObjectMap, attack: []const u8, damage: []const u8, resistance: []const u8) !CombatStats { - return CombatStats{ - .attack = try json_utils.getIntegerRequired(object, attack), - .damage = try json_utils.getIntegerRequired(object, damage), - .resistance = try json_utils.getIntegerRequired(object, resistance), - }; - } -}; - -pub const Equipment = struct { - pub const Consumable = struct { - id: ?ItemId, - quantity: i64, - - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, name: []const u8, quantity: []const u8) !Consumable { - return Consumable{ - .id = try getItemId(api, obj, name), - .quantity = try json_utils.getIntegerRequired(obj, quantity), - }; - } - }; - - weapon: ?ItemId, - shield: ?ItemId, - helmet: ?ItemId, - body_armor: ?ItemId, - leg_armor: ?ItemId, - boots: ?ItemId, - - ring1: ?ItemId, - ring2: ?ItemId, - amulet: ?ItemId, - - artifact1: ?ItemId, - artifact2: ?ItemId, - artifact3: ?ItemId, - - consumable1: Consumable, - consumable2: Consumable, - - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Equipment { - return Equipment{ - .weapon = try getItemId(api, obj, "weapon_slot"), - .shield = try getItemId(api, obj, "shield_slot"), - .helmet = try getItemId(api, obj, "helmet_slot"), - .body_armor = try getItemId(api, obj, "body_armor_slot"), - .leg_armor = try getItemId(api, obj, "leg_armor_slot"), - .boots = try getItemId(api, obj, "boots_slot"), - .ring1 = try getItemId(api, obj, "ring1_slot"), - .ring2 = try getItemId(api, obj, "ring2_slot"), - .amulet = try getItemId(api, obj, "amulet_slot"), - .artifact1 = try getItemId(api, obj, "artifact1_slot"), - .artifact2 = try getItemId(api, obj, "artifact2_slot"), - .artifact3 = try getItemId(api, obj, "artifact3_slot"), - .consumable1 = try Consumable.parse(api, obj, "consumable1_slot", "consumable1_slot_quantity"), - .consumable2 = try Consumable.parse(api, obj, "consumable2_slot", "consumable2_slot_quantity"), - }; - } -}; - -pub const Inventory = struct { - const slot_count = 20; - - pub const Slot = struct { - id: ?ItemId, - 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 = @intCast(quantity), - }; - } - }; - - slots: [slot_count]Slot, - - fn parse(api: *ArtifactsAPI, slots_array: json.Array) !Inventory { - assert(slots_array.items.len == Inventory.slot_count); - - var inventory: Inventory = undefined; - - for (0.., slots_array.items) |i, slot_value| { - const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType; - inventory.slots[i] = try Slot.parse(api, slot_obj); - } - - return inventory; - } - - fn findSlot(self: *Inventory, id: ItemId) ?*Slot { - for (&self.slots) |*slot| { - if (slot.id == id) { - return slot; - } - } - return null; - } - - pub fn removeItem(self: *Inventory, id: ItemId, quantity: u64) void { - const slot = self.findSlot(id) orelse unreachable; - assert(slot.quantity >= quantity); - - slot.quantity -= quantity; - if (slot.quantity == 0) { - slot.id = null; - } - } - - pub fn addItem(self: *Inventory, id: ItemId, quantity: u64) void { - if (self.findSlot(id)) |slot| { - slot.quantity += quantity; - } else { - var empty_slot: ?*Slot = null; - for (&self.slots) |*slot| { - if (slot.id == null) { - empty_slot = slot; - } - } - - assert(empty_slot != null); - empty_slot.?.id = id; - 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, - -name: []u8, -skin: []u8, -account: ?[]u8, -total_xp: i64, -gold: i64, -hp: i64, -haste: i64, -x: i64, -y: i64, -cooldown_expiration: f64, - -combat: SkillStats, -mining: SkillStats, -woodcutting: SkillStats, -fishing: SkillStats, -weaponcrafting: SkillStats, -gearcrafting: SkillStats, -jewelrycrafting: SkillStats, -cooking: SkillStats, - -water: CombatStats, -fire: CombatStats, -earth: CombatStats, -air: CombatStats, - -equipment: Equipment, - -inventory_max_items: i64, -inventory: Inventory, - -pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, child_allocator: Allocator) !Character { - var arena = try child_allocator.create(std.heap.ArenaAllocator); - errdefer child_allocator.destroy(arena); - - arena.* = std.heap.ArenaAllocator.init(child_allocator); - errdefer arena.deinit(); - - const allocator = arena.allocator(); - - const inventory = json_utils.getArray(obj, "inventory") orelse return error.MissingProperty; - const cooldown_expiration = json_utils.getString(obj, "cooldown_expiration") orelse return error.MissingProperty; - - return Character{ - .arena = arena, - .account = try json_utils.dupeString(allocator, obj, "account"), - .name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty, - .skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty, - - .total_xp = try json_utils.getIntegerRequired(obj, "total_xp"), - .gold = try json_utils.getIntegerRequired(obj, "gold"), - .hp = try json_utils.getIntegerRequired(obj, "hp"), - .haste = try json_utils.getIntegerRequired(obj, "haste"), - .x = try json_utils.getIntegerRequired(obj, "x"), - .y = try json_utils.getIntegerRequired(obj, "y"), - .cooldown_expiration = parseDateTime(cooldown_expiration) orelse return error.InvalidDateTime, - - .combat = try SkillStats.parse(obj, "level", "xp", "max_xp"), - .mining = try SkillStats.parse(obj, "mining_level", "mining_xp", "mining_max_xp"), - .woodcutting = try SkillStats.parse(obj, "woodcutting_level", "woodcutting_xp", "woodcutting_max_xp"), - .fishing = try SkillStats.parse(obj, "fishing_level", "fishing_xp", "fishing_max_xp"), - .weaponcrafting = try SkillStats.parse(obj, "weaponcrafting_level", "weaponcrafting_xp", "weaponcrafting_max_xp"), - .gearcrafting = try SkillStats.parse(obj, "gearcrafting_level", "gearcrafting_xp", "gearcrafting_max_xp"), - .jewelrycrafting = try SkillStats.parse(obj, "jewelrycrafting_level", "jewelrycrafting_xp", "jewelrycrafting_max_xp"), - .cooking = try SkillStats.parse(obj, "cooking_level", "cooking_xp", "cooking_max_xp"), - - .water = try CombatStats.parse(obj, "attack_water", "dmg_water", "res_water"), - .fire = try CombatStats.parse(obj, "attack_fire", "dmg_fire", "res_fire"), - .earth = try CombatStats.parse(obj, "attack_earth", "dmg_earth", "res_earth"), - .air = try CombatStats.parse(obj, "attack_air", "dmg_air", "res_air"), - - .equipment = try Equipment.parse(api, obj), - - .inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty, - .inventory = try Inventory.parse(api, inventory) - }; -} - -pub fn deinit(self: *Character) void { - var child_allocator = self.arena.child_allocator; - self.arena.deinit(); - child_allocator.destroy(self.arena); -} - -pub fn getItemCount(self: *const Character) u64 { - var count: u64 = 0; - for (self.inventory.slots) |slot| { - count += slot.quantity; - } - return count; -} diff --git a/src/date_time/parse.zig b/src/date_time/parse.zig new file mode 100644 index 0000000..3f6e79a --- /dev/null +++ b/src/date_time/parse.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn parseDateTime(datetime: []const u8) ?f64 { + const time_h = @cImport({ + @cDefine("_XOPEN_SOURCE", "700"); + @cInclude("stddef.h"); + @cInclude("stdio.h"); + @cInclude("time.h"); + @cInclude("timegm.h"); + }); + + var buffer: [256]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&buffer); + var allocator = fba.allocator(); + + const datetime_z = allocator.dupeZ(u8, datetime) catch return null; + + var tm: time_h.tm = .{}; + const s = time_h.strptime(datetime_z, "%Y-%m-%dT%H:%M:%S.", &tm); + if (s == null) { + return null; + } + + const s_len = std.mem.len(s); + if (s[s_len-1] != 'Z') { + return null; + } + + const milliseconds_str = s[0..(s_len-1)]; + var milliseconds: f64 = @floatFromInt(std.fmt.parseUnsigned(u32, milliseconds_str, 10) catch return null); + while (milliseconds >= 1) { + milliseconds /= 10; + } + + const seconds: f64 = @floatFromInt(time_h.my_timegm(&tm)); + + return seconds + milliseconds; +} + +test "parse date time" { + try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?); +} diff --git a/src/timegm.c b/src/date_time/timegm.c similarity index 100% rename from src/timegm.c rename to src/date_time/timegm.c diff --git a/src/timegm.h b/src/date_time/timegm.h similarity index 100% rename from src/timegm.h rename to src/date_time/timegm.h diff --git a/src/main.zig b/src/main.zig index ca0831d..28b4f0c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,45 +1,98 @@ const std = @import("std"); -const assert = std.debug.assert; -const ArtifactsAPI = @import("artifacts.zig"); const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const Position = @import("./api/position.zig"); +const Server = @import("./api/server.zig"); // pub const std_options = .{ .log_level = .debug }; -const Position = struct { - x: i64, - y: i64, +const QueuedAction = union(enum) { + move: Position, + fight, + gather, + deposit_gold: u64, + deposit_item: Server.ItemIdQuantity, + withdraw_item: Server.ItemIdQuantity, + craft_item: Server.ItemIdQuantity, +}; - fn init(x: i64, y: i64) Position { - return Position{ - .x = x, - .y = y +const CharacterBrain = struct { + name: []const u8, + routine: union (enum) { + idle, + + fight: struct { + at: Position, + target: Server.ItemIdQuantity, + progress: u64 = 0, + }, + gather: struct { + at: Position, + target: Server.ItemIdQuantity, + progress: u64 = 0, + }, + craft: struct { + target: Server.ItemIdQuantity, + progress: u64 = 0, + }, + }, + action_queue: std.ArrayList(QueuedAction), + + fn init(allocator: Allocator, name: []const u8) !CharacterBrain { + return CharacterBrain{ + .name = try allocator.dupe(u8, name), + .routine = .idle, + .action_queue = std.ArrayList(QueuedAction).init(allocator), }; } - fn eql(self: Position, other: Position) bool { - return self.x == other.x and self.y == other.y; + fn deinit(self: CharacterBrain) void { + const allocator = self.action_queue.allocator; + allocator.free(self.name); + self.action_queue.deinit(); } -}; -const QueuedAction = union(enum) { - move: Position, - attack, - gather, - deposit_gold: u64, - deposit_item: ArtifactsAPI.ItemIdQuantity, - withdraw_item: ArtifactsAPI.ItemIdQuantity, - craft_item: ArtifactsAPI.ItemIdQuantity, -}; + fn performNextAction(self: *CharacterBrain, api: *Server) !void { + assert(self.action_queue.items.len > 0); -const ActionQueue = std.ArrayList(QueuedAction); + const log = std.log.default; -const ManagedCharacter = struct { - character: ArtifactsAPI.Character, - action_queue: ActionQueue, - cooldown_expires_at: f64, + switch (self.action_queue.items[0]) { + .fight => { + log.debug("{s} attacks", .{self.name}); + _ = try api.actionFight(self.name); + }, + .move => |pos| { + log.debug("move {s} to ({}, {})", .{self.name, pos.x, pos.y}); + _ = try api.actionMove(self.name, pos.x, pos.y); + }, + .deposit_gold => |quantity| { + log.debug("deposit {} gold from {s}", .{quantity, self.name}); + _ = try api.actionBankDepositGold(self.name, quantity); + }, + .deposit_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + log.debug("deposit {s} (x{}) from {s}", .{code, item.quantity, self.name}); + _ = try api.actionBankDepositItem(self.name, code, item.quantity); + }, + .withdraw_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + log.debug("withdraw {s} (x{}) from {s}", .{code, item.quantity, self.name}); + _ = try api.actionBankWithdrawItem(self.name, code, item.quantity); + }, + .gather => { + log.debug("{s} gathers", .{self.name}); + _ = try api.actionGather(self.name); + }, + .craft_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + log.debug("craft {s} (x{}) from {s}", .{code, item.quantity, self.name}); + _ = try api.actionCraft(self.name, code, item.quantity); + } + } - pub fn position(self: *const ManagedCharacter) Position { - return Position{ .x = self.character.x, .y = self.character.y }; + _ = self.action_queue.orderedRemove(0); } }; @@ -48,128 +101,100 @@ fn currentTime() f64 { return timestamp / std.time.ms_per_s; } -const Manager = struct { +const GoalManager = struct { + api: *Server, allocator: Allocator, - characters: std.ArrayList(ManagedCharacter), - api: *ArtifactsAPI, - known_items: std.StringHashMap(ArtifactsAPI.Item), + characters: std.ArrayList(CharacterBrain), + expiration_margin: f64 = 0.1, // 100ms - bank_position: Position = .{ .x = 4, .y = 1 }, - - fn init(allocator: Allocator, api: *ArtifactsAPI) Manager { - return Manager{ - .allocator = allocator, + fn init(api: *Server, allocator: Allocator) GoalManager { + return GoalManager{ .api = api, - .known_items = std.StringHashMap(ArtifactsAPI.Item).init(allocator), - .characters = std.ArrayList(ManagedCharacter).init(allocator), + .allocator = allocator, + .characters = std.ArrayList(CharacterBrain).init(allocator) }; } - fn addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void { - try self.characters.append(ManagedCharacter{ - .character = character, - .action_queue = std.ArrayList(QueuedAction).init(self.allocator), - .cooldown_expires_at = character.cooldown_expiration - }); + fn addCharacter(self: *GoalManager, name: []const u8) !void { + const character = try CharacterBrain.init(self.allocator, name); + try self.characters.append(character); } - fn poll(self: *Manager) ?*ManagedCharacter { - if (self.characters.items.len == 0) return null; + fn deinit(self: GoalManager) void { + for (self.characters.items) |brain| { + brain.deinit(); + } + self.characters.deinit(); + } - var earliest_expiration = self.characters.items[0].cooldown_expires_at; + fn runNextAction(self: *GoalManager) !void { + var earliest_cooldown: f64 = 0; + var earliest_character: ?*CharacterBrain = null; + for (self.characters.items) |*brain| { + if (brain.action_queue.items.len == 0) continue; - var now = currentTime(); - for (self.characters.items) |managed_character| { - earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at); + const character = self.api.findCharacter(brain.name).?; + if (earliest_character == null or earliest_cooldown > character.cooldown_expiration) { + earliest_character = brain; + earliest_cooldown = character.cooldown_expiration; + } } - if (earliest_expiration > now) { - const duration_s = earliest_expiration - now; + if (earliest_character == null) return; + + const now = currentTime(); + if (earliest_cooldown > now) { + const duration_s = earliest_cooldown - now + self.expiration_margin; const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s)); std.log.debug("waiting for {d:.3}s", .{duration_s}); std.time.sleep(std.time.ns_per_ms * duration_ms); } - const cooldown_margin = 0.1; // 100ms - - now = currentTime(); - for (self.characters.items) |*managed_character| { - if (now - managed_character.cooldown_expires_at >= -cooldown_margin) { - return managed_character; - } - } - - return null; - } - - 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 }, - }; + try earliest_character.?.performNextAction(self.api); } }; -fn moveIfNeeded(char: *ManagedCharacter, pos: Position) !bool { - if (char.position().eql(pos)) { +const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically + +fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 { + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + if (args.len < 2) { + return null; + } + + const filename = args[1]; + const cwd = std.fs.cwd(); + var token_buffer: [256]u8 = undefined; + const token = try cwd.readFile(filename, &token_buffer); + + return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t ")); +} + +fn moveIfNeeded(api: *Server, brain: *CharacterBrain, pos: Position) !bool { + const character = api.findCharacter(brain.name).?; + + if (character.position.eql(pos)) { return false; } - try char.action_queue.append(.{ .move = pos }); + try brain.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; +fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool { + const character = api.findCharacter(brain.name).?; + const action_queue = &brain.action_queue; // Deposit items and gold to bank if full if (character.getItemCount() == 0) { return false; } - var character_pos = managed_char.position(); - - if (!character_pos.eql(manager.bank_position)) { - try action_queue.append(.{ .move = manager.bank_position }); + if (!character.position.eql(bank_position)) { + try action_queue.append(.{ .move = bank_position }); } for (character.inventory.slots) |slot| { @@ -185,52 +210,51 @@ fn depositItemsToBank(manager: *Manager, managed_char: *ManagedCharacter) !bool return true; } -fn depositIfFull(manager: *Manager, char: *ManagedCharacter) !bool { - const character = char.character; +fn depositIfFull(api: *Server, brain: *CharacterBrain) !bool { + const character = api.findCharacter(brain.name).?; if (character.getItemCount() < character.inventory_max_items) { return false; } - _ = try depositItemsToBank(manager, char); + _ = try depositItemsToBank(api, brain); if (character.gold > 0) { - try char.action_queue.append(.{ .deposit_gold = @intCast(character.gold) }); + try brain.action_queue.append(.{ .deposit_gold = @intCast(character.gold) }); } + + return true; } -fn attackChickenRoutine(manager: *Manager, managed_char: *ManagedCharacter) !void { - const chicken_pos = Position{ .x = 0, .y = 1 }; - - // Deposit items and gold to bank if full - if (try depositIfFull(manager, managed_char)) { +fn fightRoutine(api: *Server, brain: *CharacterBrain, enemy_position: Position) !void { + if (try depositIfFull(api, brain)) { return; } - // Go to chickens - if (try moveIfNeeded(managed_char, chicken_pos)) { + if (try moveIfNeeded(api, brain, enemy_position)) { return; } - // Attack chickens - try managed_char.action_queue.append(.{ .attack = {} }); + try brain.action_queue.append(.{ .fight = {} }); } -fn gatherResourceRoutine(manager: *Manager, managed_char: *ManagedCharacter, resource_pos: Position) !void { - if (try depositIfFull(manager, managed_char)) { +fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Position) !void { + if (try depositIfFull(api, brain)) { return; } - if (try moveIfNeeded(managed_char, resource_pos)) { + if (try moveIfNeeded(api, brain, resource_position)) { return; } - try managed_char.action_queue;.append(.{ .gather = {} }); + try brain.action_queue.append(.{ .gather = {} }); } -fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const ArtifactsAPI.ItemIdQuantity) !bool { +fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.ItemIdQuantity) !bool { + var character = api.findCharacter(brain.name).?; + var has_all_items = true; for (items) |item_quantity| { - const inventory_quantity = char.character.inventory.getItem(item_quantity.id); + const inventory_quantity = character.inventory.getItem(item_quantity.id); if(inventory_quantity < item_quantity.quantity) { has_all_items = false; break; @@ -238,14 +262,14 @@ fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const A } if (has_all_items) return false; - if (try moveIfNeeded(char, manager.bank_position)) { + if (try moveIfNeeded(api, brain, bank_position)) { return true; } for (items) |item_quantity| { - const inventory_quantity = char.character.inventory.getItem(item_quantity.id); + const inventory_quantity = character.inventory.getItem(item_quantity.id); if(inventory_quantity < item_quantity.quantity) { - try char.action_queue.append(.{ .withdraw_item = .{ + try brain.action_queue.append(.{ .withdraw_item = .{ .id = item_quantity.id, .quantity = item_quantity.quantity - inventory_quantity, }}); @@ -255,26 +279,37 @@ fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const A return true; } -fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool { - const inventory_quantity = char.character.inventory.getItem(id); +fn craftItem(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !bool { + var character = api.findCharacter(brain.name).?; + + const inventory_quantity = 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; + const code = api.getItemCode(id) orelse return error.InvalidItemId; + const item = try api.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)) { + // TODO: Figure this out dynamically + const workstation = switch (recipe.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 }, + }; + + if (try moveIfNeeded(api, brain, workstation)) { return true; } - try char.action_queue.append(.{ .craft_item = .{ + try brain.action_queue.append(.{ .craft_item = .{ .id = id, .quantity = quantity - inventory_quantity }}); @@ -282,14 +317,17 @@ fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId return true; } -fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool { - const inventory_quantity = char.character.inventory.getItem(id); +fn craftRoutine(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !void { + var character = api.findCharacter(brain.name).?; + const inventory_quantity = character.inventory.getItem(id); if (inventory_quantity >= quantity) { - return false; + if (try depositItemsToBank(api, brain)) { + return; + } } - const code = manager.api.getItemCode(id) orelse return error.InvalidItemId; - const target_item = try manager.getItem(code) orelse return error.ItemNotFound; + const code = api.getItemCode(id) orelse return error.InvalidItemId; + const target_item = try api.getItem(code) orelse return error.ItemNotFound; if (target_item.craft == null) { return error.NotCraftable; } @@ -302,15 +340,13 @@ fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAP needed_item.quantity *= quantity; } - if (try withdrawFromBank(manager, char, needed_items.constSlice())) { - return true; + if (try withdrawFromBank(api, brain, needed_items.constSlice())) { + return; } - if (try craftItem(manager, char, id, quantity)) { - return true; + if (try craftItem(api, brain, id, quantity)) { + return; } - - return true; } pub fn main() !void { @@ -318,145 +354,87 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var api = try ArtifactsAPI.init(allocator); + var api = try Server.init(allocator); defer api.deinit(); - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); + const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken; + defer allocator.free(token); - if (args.len >= 2) { - const filename = args[1]; - const cwd = std.fs.cwd(); + try api.setToken(token); - var token_buffer: [256]u8 = undefined; - const token = try cwd.readFile(filename, &token_buffer); - try api.setToken(std.mem.trim(u8,token,"\n\t ")); + var goal_manager = GoalManager.init(&api, allocator); + defer goal_manager.deinit(); + + const chars = try api.listMyCharacters(); + defer chars.deinit(); + + for (chars.items) |char| { + try goal_manager.addCharacter(char.name); } - if (api.token == null) { - return error.MissingToken; - } + goal_manager.characters.items[0].routine = .{ + .fight = .{ + .at = Position.init(0, 1), + .target = undefined + }, + }; - var manager = Manager.init(allocator, &api); - defer manager.deinit(); + goal_manager.characters.items[1].routine = .{ + .gather = .{ + .at = Position.init(-1, 0), + .target = undefined + } + }; - const status = try api.getServerStatus(); - defer status.deinit(); + goal_manager.characters.items[2].routine = .{ + .gather = .{ + .at = Position.init(2, 0), + .target = undefined + } + }; - std.log.info("Server status: {s} v{s}", .{ status.status, status.version }); - std.log.info("Characters online: {}", .{ status.characters_online }); + goal_manager.characters.items[3].routine = .{ + .gather = .{ + .at = Position.init(4, 2), + .target = undefined + } + }; - const characters = try api.listMyCharacters(allocator); - defer characters.deinit(); + goal_manager.characters.items[4].routine = .{ + .fight = .{ + .at = Position.init(0, 1), + .target = undefined + }, + }; - // for (characters.list.items) |character| { - // try manager.addCharacter(character); - // } - try manager.addCharacter(characters.list.items[0]); + // goal_manager.characters.items[2].routine = .{ + // .craft = .{ + // .target = .{ + // .quantity = 3, + // .id = try api.getItemId("copper"), + // } + // } + // }; std.log.info("Starting main loop", .{}); - while (manager.poll()) |char| { - if (char.action_queue.items.len > 0) { - const action = char.action_queue.items[0]; + while (true) { + try goal_manager.runNextAction(); - var cooldown: ArtifactsAPI.Cooldown = undefined; - switch (action) { - .attack => { - std.log.debug("{s} attacks", .{char.character.name}); - var result = try api.actionFight(char.character.name); + for (goal_manager.characters.items) |*character| { + if (character.action_queue.items.len > 0) continue; - cooldown = result.cooldown; - char.character.gold += result.fight.gold; - for (result.fight.drops.slice()) |item| { - char.character.inventory.addItem(item.id, @intCast(item.quantity)); - } + switch (character.routine) { + .idle => {}, + .fight => |args| { + try fightRoutine(&api, character, args.at); }, - .move => |pos| { - std.log.debug("move {s} to ({}, {})", .{char.character.name, pos.x, pos.y}); - var result = try api.actionMove(char.character.name, pos.x, pos.y); - defer result.deinit(); - - cooldown = result.cooldown; - char.character.x = pos.x; - char.character.y = pos.y; + .gather => |args| { + try gatherRoutine(&api, character, args.at); }, - .deposit_gold => |quantity| { - std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name}); - const result = try api.actionBankDepositGold(char.character.name, quantity); - - cooldown = result.cooldown; - char.character.gold -= @intCast(quantity); - assert(char.character.gold >= 0); - }, - .deposit_item => |item| { - const code = api.getItemCode(item.id) orelse return error.ItemNotFound; - 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; - 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()); + .craft => |args| { + try craftRoutine(&api, character, args.target.id, args.target.quantity); } } - - char.cooldown_expires_at = cooldown.expiration; - - _ = char.action_queue.orderedRemove(0); - 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")) { - try gatherResourceRoutine(char, .{ .x = 2, .y = 0 }); // Copper ore - } else if (std.mem.eql(u8, char.character.name, "Diana")) { - try gatherResourceRoutine(char, .{ .x = 4, .y = 2 }); // Gudgeon fish - } else { - try attackChickenRoutine(char); } } }