From 8cd5fb44c865044b320ced6b5178f07d37394bc0 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Thu, 29 Aug 2024 22:39:35 +0300 Subject: [PATCH] implement graceful handling of API errors --- src/api/character.zig | 10 +- src/api/inventory.zig | 96 --------- src/api/server.zig | 59 +++--- src/api/slot.zig | 23 ++ src/api/slot_array.zig | 115 ++++++++++ src/main.zig | 470 +++++++++++++++++++++++++++++++++++------ 6 files changed, 574 insertions(+), 199 deletions(-) delete mode 100644 src/api/inventory.zig create mode 100644 src/api/slot.zig create mode 100644 src/api/slot_array.zig diff --git a/src/api/character.zig b/src/api/character.zig index 0f74339..6b7ff9b 100644 --- a/src/api/character.zig +++ b/src/api/character.zig @@ -11,7 +11,9 @@ 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 BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray; + +const Inventory = BoundedSlotsArray(20); const Character = @This(); @@ -94,11 +96,7 @@ pub fn deinit(self: *Character) void { } pub fn getItemCount(self: *const Character) u64 { - var count: u64 = 0; - for (self.inventory.slots) |slot| { - count += slot.quantity; - } - return count; + return self.inventory.totalQuantity(); } pub fn format( diff --git a/src/api/inventory.zig b/src/api/inventory.zig deleted file mode 100644 index 29468a0..0000000 --- a/src/api/inventory.zig +++ /dev/null @@ -1,96 +0,0 @@ -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/api/server.zig b/src/api/server.zig index 6eaba07..49fa771 100644 --- a/src/api/server.zig +++ b/src/api/server.zig @@ -8,6 +8,8 @@ pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime; const json_utils = @import("json_utils.zig"); pub const Character = @import("character.zig"); +const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray; +pub const Slot = @import("./slot.zig"); // Specification: https://api.artifactsmmo.com/docs @@ -252,16 +254,12 @@ pub const Cooldown = struct { pub const FightResult = struct { const Details = struct { - const DroppedItem = struct { - id: ItemId, - quantity: i64 - }; - const Result = enum { win, lose }; + const Drops = BoundedSlotsArray(8); xp: i64, gold: i64, - drops: std.BoundedArray(DroppedItem, 8), + drops: Drops, result: Result, fn parse(api: *Server, obj: json.ObjectMap) !Details { @@ -271,24 +269,16 @@ pub const FightResult = struct { result_enum = .win; } else if (std.mem.eql(u8, result, "win")) { result_enum = .lose; + } else { + return error.InvalidProperty; } - var drops = std.BoundedArray(DroppedItem, 8).init(0) catch unreachable; const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty; - for (drops_obj.items) |drop_value| { - const drop_obj = json_utils.asObject(drop_value) orelse return error.MissingProperty; - const code = try json_utils.getStringRequired(drop_obj, "code"); - - try drops.append(DroppedItem{ - .id = try api.getItemId(code), - .quantity = try json_utils.getIntegerRequired(drop_obj, "quantity") - }); - } return Details{ .xp = try json_utils.getIntegerRequired(obj, "xp"), .gold = try json_utils.getIntegerRequired(obj, "gold"), - .drops = drops, + .drops = try Drops.parse(api, drops_obj), .result = result_enum, }; } @@ -322,6 +312,7 @@ pub const FightResult = struct { } }; +// TODO: Replace this with ItemSlot struct pub const ItemIdQuantity = struct { id: ItemId, quantity: u64, @@ -338,29 +329,18 @@ pub const ItemIdQuantity = struct { } }; -const BoundedItems = std.BoundedArray(ItemIdQuantity, 8); -fn parseSimpleItemList(api: *Server, array: json.Array) !BoundedItems { - var items = BoundedItems.init(0) catch unreachable; - - for (array.items) |item_value| { - const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty; - - try items.append(try ItemIdQuantity.parse(api, item_obj)); - } - - return items; -} - pub const SkillResultDetails = struct { + const Items = BoundedSlotsArray(8); + xp: i64, - items: BoundedItems, + items: Items, fn parse(api: *Server, obj: json.ObjectMap) !SkillResultDetails { const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty; return SkillResultDetails{ .xp = try json_utils.getIntegerRequired(obj, "xp"), - .items = try parseSimpleItemList(api, items), + .items = try Items.parse(api, items), }; } }; @@ -640,10 +620,12 @@ pub const MapResult = struct { pub const Item = struct { pub const Recipe = struct { + const Items = BoundedSlotsArray(8); + skill: Skill, level: u64, quantity: u64, - items: BoundedItems, + items: Items, pub fn parse(api: *Server, obj: json.ObjectMap) !Recipe { const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty; @@ -659,7 +641,7 @@ pub const Item = struct { .skill = Skill.parse(skill) orelse return error.InvalidSkill, .level = @intCast(level), .quantity = @intCast(quantity), - .items = try parseSimpleItemList(api, items) + .items = try Items.parse(api, items) }; } }; @@ -1094,6 +1076,15 @@ pub fn findCharacter(self: *const Server, name: []const u8) ?Character { return null; } +pub fn findCharacterPtr(self: *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); } diff --git a/src/api/slot.zig b/src/api/slot.zig new file mode 100644 index 0000000..1f2dea5 --- /dev/null +++ b/src/api/slot.zig @@ -0,0 +1,23 @@ +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 ItemSlot = @This(); + +id: ItemId, +quantity: u64, + +pub fn parse(api: *Server, slot_obj: json.ObjectMap) !?ItemSlot { + const code = try json_utils.getStringRequired(slot_obj, "code"); + if (code.len == 0) return null; + + const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity"); + if (quantity < 0) return error.InvalidQuantity; + + return ItemSlot{ + .id = try api.getItemId(code), + .quantity = @intCast(quantity), + }; +} diff --git a/src/api/slot_array.zig b/src/api/slot_array.zig new file mode 100644 index 0000000..772283c --- /dev/null +++ b/src/api/slot_array.zig @@ -0,0 +1,115 @@ +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 Slot = @import("./slot.zig"); + +pub fn BoundedSlotsArray(comptime slot_count: u32) type { + const Slots = std.BoundedArray(Slot, slot_count); + + return struct { + slots: Slots, + + fn init() @This() { + return @This(){ + .slots = Slots.init(0) catch unreachable + }; + } + + pub fn parse(api: *Server, slots_array: json.Array) !@This() { + var slots = Slots.init(0) catch unreachable; + + for (slots_array.items) |slot_value| { + const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType; + + if (try Slot.parse(api, slot_obj)) |slot| { + try slots.append(slot); + } + } + + return @This(){ .slots = slots }; + } + + fn findSlotIndex(self: *const @This(), id: ItemId) ?usize { + for (0.., self.slots.slice()) |i, *slot| { + if (slot.id == id) { + return i; + } + } + + return null; + } + + fn findSlot(self: *@This(), id: ItemId) ?*Slot { + if (self.findSlotIndex(id)) |index| { + return &self.slots.buffer[index]; + } + + return null; + } + + pub fn remove(self: *@This(), id: ItemId, quantity: u64) void { + const slot_index = self.findSlotIndex(id) orelse unreachable; + const slot = self.slots.get(slot_index); + assert(slot.quantity >= quantity); + + slot.quantity -= quantity; + if (slot.quantity == 0) { + self.slots.swapRemove(slot_index); + } + } + + pub fn add(self: *@This(), 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 addSlice(self: *@This(), items: []const Server.ItemIdQuantity) void { + for (items) |item| { + self.add(item.id, item.quantity); + } + } + + pub fn removeSlice(self: *@This(), items: []const Server.ItemIdQuantity) void { + for (items) |item| { + self.remove(item.id, item.quantity); + } + } + + pub fn getQuantity(self: *const @This(), id: ItemId) u64 { + if (self.findSlotIndex(id)) |index| { + return self.slots.get(index).quantity; + } + + return 0; + } + + pub fn totalQuantity(self: *const @This()) u64 { + var count: u64 = 0; + for (self.slots.constSlice()) |slot| { + count += slot.quantity; + } + return count; + } + + pub fn slice(self: *@This()) []Slot { + return self.slots.slice(); + } + }; +} + diff --git a/src/main.zig b/src/main.zig index 28b4f0c..9855539 100644 --- a/src/main.zig +++ b/src/main.zig @@ -17,6 +17,89 @@ const QueuedAction = union(enum) { craft_item: Server.ItemIdQuantity, }; +const QueuedActionResult = union(enum) { + move: Server.MoveError!Server.MoveResult, + fight: Server.FightError!Server.FightResult, + gather: Server.GatherError!Server.GatherResult, + deposit_gold: Server.BankDepositGoldError!Server.GoldTransactionResult, + deposit_item: Server.BankDepositItemError!Server.ItemTransactionResult, + withdraw_item: Server.BankWithdrawItemError!Server.ItemTransactionResult, + craft_item: Server.CraftError!Server.CraftResult, + + const Tag = @typeInfo(QueuedActionResult).Union.tag_type.?; + + fn fieldType(comptime kind: Tag) type { + const field_type = std.meta.fields(QueuedActionResult)[@intFromEnum(kind)].type; + return @typeInfo(field_type).ErrorUnion.payload; + } + + pub fn get(self: QueuedActionResult, comptime kind: Tag) ?fieldType(kind) { + return switch (self) { + kind => |v| v catch null, + else => null + }; + } + + fn getMove() Server.MoveResult { + } +}; + +comptime { + assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len); +} + +fn performAction(api: *Server, name: []const u8, action: QueuedAction) !QueuedActionResult { + const log = std.log.default; + + switch (action) { + .fight => { + log.debug("[{s}] attack", .{name}); + return .{ + .fight = api.actionFight(name) + }; + }, + .move => |pos| { + log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y}); + return .{ + .move = api.actionMove(name, pos.x, pos.y) + }; + }, + .deposit_gold => |quantity| { + log.debug("[{s}] deposit {} gold", .{name, quantity}); + return .{ + .deposit_gold = api.actionBankDepositGold(name, quantity) + }; + }, + .deposit_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + log.debug("[{s}] deposit {s} (x{})", .{name, code, item.quantity}); + return .{ + .deposit_item = api.actionBankDepositItem(name, code, item.quantity) + }; + }, + .withdraw_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + log.debug("[{s}] withdraw {s} (x{})", .{name, code, item.quantity}); + return .{ + .withdraw_item = api.actionBankWithdrawItem(name, code, item.quantity) + }; + }, + .gather => { + log.debug("[{s}] gather", .{name}); + return .{ + .gather = api.actionGather(name) + }; + }, + .craft_item => |item| { + const code = api.getItemCode(item.id) orelse return error.ItemNotFound; + log.debug("[{s}] craft {s} (x{})", .{name, code, item.quantity}); + return .{ + .craft_item = api.actionCraft(name, code, item.quantity) + }; + } + } +} + const CharacterBrain = struct { name: []const u8, routine: union (enum) { @@ -54,45 +137,285 @@ const CharacterBrain = struct { } fn performNextAction(self: *CharacterBrain, api: *Server) !void { + const log = std.log.default; assert(self.action_queue.items.len > 0); - const log = std.log.default; + const APIError = Server.APIError; - switch (self.action_queue.items[0]) { - .fight => { - log.debug("{s} attacks", .{self.name}); - _ = try api.actionFight(self.name); + const retry_delay = 0.5; // 500ms + var character = api.findCharacterPtr(self.name).?; + + const action_result = try performAction(api, self.name, self.action_queue.items[0]); + switch (action_result) { + .fight => |result| { + const FightError = Server.FightError; + _ = result catch |err| switch (err) { + FightError.CharacterInCooldown, + FightError.CharacterIsBusy => { + // A bit too eager, retry action + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry fighting", .{self.name}); + return; + }, + + FightError.CharacterIsFull, + FightError.MonsterNotFound => { + // Re-evaluate what the character should do, something is not right. + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + return; + }, + + FightError.CharacterNotFound, + APIError.ServerUnavailable, + APIError.RequestFailed, + APIError.ParseFailed, + APIError.OutOfMemory => { + // Welp... Abondon ship. Bail. Bail + return err; + } + }; }, - .move => |pos| { - log.debug("move {s} to ({}, {})", .{self.name, pos.x, pos.y}); - _ = try api.actionMove(self.name, pos.x, pos.y); + .move => |result| { + const MoveError = Server.MoveError; + _ = result catch |err| switch (err) { + MoveError.CharacterIsBusy, + MoveError.CharacterInCooldown => { + // A bit too eager, retry action + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry moving", .{self.name}); + return; + }, + + MoveError.CharacterAtDestination => { + // Not great, but I guess the goal achieved? The character is at the desired location. + log.warn("[{s}] tried to move, but already at destination", .{self.name}); + }, + + MoveError.MapNotFound => { + // Re-evaluate what the character should do, something is not right. + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + return; + }, + + MoveError.CharacterNotFound, + APIError.ServerUnavailable, + APIError.RequestFailed, + APIError.ParseFailed, + APIError.OutOfMemory => { + // Welp... Abondon ship. Bail. Bail + return err; + } + }; }, - .deposit_gold => |quantity| { - log.debug("deposit {} gold from {s}", .{quantity, self.name}); - _ = try api.actionBankDepositGold(self.name, quantity); + .deposit_gold => |result| { + const BankDepositGoldError = Server.BankDepositGoldError; + _ = result catch |err| switch (err) { + BankDepositGoldError.BankIsBusy, + BankDepositGoldError.CharacterIsBusy, + BankDepositGoldError.CharacterInCooldown => { + // A bit too eager, retry action + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry depositing gold", .{self.name}); + return; + }, + + BankDepositGoldError.NotEnoughGold, + BankDepositGoldError.BankNotFound => { + // Re-evaluate what the character should do, something is not right. + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + return; + }, + + BankDepositGoldError.CharacterNotFound, + APIError.ServerUnavailable, + APIError.RequestFailed, + APIError.ParseFailed, + APIError.OutOfMemory => { + // Welp... Abondon ship. Bail. Bail + return err; + } + }; }, - .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); + .deposit_item => |result| { + const BankDepositItemError = Server.BankDepositItemError; + _ = result catch |err| switch (err) { + BankDepositItemError.BankIsBusy, + BankDepositItemError.CharacterIsBusy, + BankDepositItemError.CharacterInCooldown => { + // A bit too eager, retry action + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry depositing item", .{self.name}); + return; + }, + + BankDepositItemError.ItemNotFound, + BankDepositItemError.NotEnoughItems, + BankDepositItemError.BankNotFound => { + // Re-evaluate what the character should do, something is not right. + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + return; + }, + + BankDepositItemError.CharacterNotFound, + APIError.ServerUnavailable, + APIError.RequestFailed, + APIError.ParseFailed, + APIError.OutOfMemory => { + // Welp... Abondon ship. Bail. Bail + return err; + } + }; }, - .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); + .withdraw_item => |result| { + const BankWithdrawItemError = Server.BankWithdrawItemError; + _ = result catch |err| switch (err) { + BankWithdrawItemError.CharacterIsBusy, + BankWithdrawItemError.CharacterInCooldown, + BankWithdrawItemError.BankIsBusy => { + // A bit too eager, retry action + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry withdrawing item", .{self.name}); + return; + }, + + BankWithdrawItemError.ItemNotFound, + BankWithdrawItemError.NotEnoughItems, + BankWithdrawItemError.CharacterIsFull, + BankWithdrawItemError.BankNotFound => { + // Re-evaluate what the character should do, something is not right. + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + return; + }, + + BankWithdrawItemError.CharacterNotFound, + APIError.ServerUnavailable, + APIError.RequestFailed, + APIError.ParseFailed, + APIError.OutOfMemory => { + // Welp... Abondon ship. Bail. Bail + return err; + } + }; }, - .gather => { - log.debug("{s} gathers", .{self.name}); - _ = try api.actionGather(self.name); + .gather => |result| { + const GatherError = Server.GatherError; + _ = result catch |err| switch (err) { + GatherError.CharacterInCooldown, + GatherError.CharacterIsBusy => { + // A bit too eager, retry action + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry withdrawing item", .{self.name}); + return; + }, + + GatherError.NotEnoughSkill, + GatherError.CharacterIsFull, + GatherError.ResourceNotFound => { + // Re-evaluate what the character should do, something is not right. + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + return; + }, + + GatherError.CharacterNotFound, + APIError.ServerUnavailable, + APIError.RequestFailed, + APIError.ParseFailed, + APIError.OutOfMemory => { + // Welp... Abondon ship. Bail. Bail + return err; + } + }; }, - .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); + .craft_item => |result| { + const CraftError = Server.CraftError; + _ = result catch |err| switch (err) { + CraftError.CharacterInCooldown, + CraftError.CharacterIsBusy => { + // A bit too eager, retry action + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry withdrawing item", .{self.name}); + return; + }, + + CraftError.RecipeNotFound, + CraftError.NotEnoughItems, + CraftError.NotEnoughSkill, + CraftError.CharacterIsFull, + CraftError.WorkshopNotFound => { + // Re-evaluate what the character should do, something is not right. + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + return; + }, + + CraftError.CharacterNotFound, + APIError.ServerUnavailable, + APIError.RequestFailed, + APIError.ParseFailed, + APIError.OutOfMemory => { + // Welp... Abondon ship. Bail. Bail + return err; + } + }; } } _ = self.action_queue.orderedRemove(0); + + self.onActionCompleted(action_result); + } + + fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void { + switch (self.routine) { + .idle => {}, + + .fight => |*args| { + if (result.get(.fight)) |r| { + const fight_result: Server.FightResult = r; + const drops = fight_result.fight.drops; + + args.progress += drops.getQuantity(args.target.id); + } + }, + .gather => |*args| { + if (result.get(.gather)) |r| { + const gather_resutl: Server.GatherResult = r; + const items = gather_resutl.details.items; + + args.progress += items.getQuantity(args.target.id); + } + }, + .craft => |*args| { + if (result.get(.craft_item)) |r| { + const craft_result: Server.CraftResult = r; + const items = craft_result.details.items; + + args.progress += items.getQuantity(args.target.id); + } + } + } + } + + fn isRoutineFinished(self: *CharacterBrain) bool { + return switch (self.routine) { + .idle => false, + + .fight => |args| { + return args.progress >= args.target.quantity; + }, + .gather => |args| { + return args.progress >= args.target.quantity; + }, + .craft => |args| { + return args.progress >= args.target.quantity; + } + }; } }; @@ -185,7 +508,7 @@ fn moveIfNeeded(api: *Server, brain: *CharacterBrain, pos: Position) !bool { } fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool { - const character = api.findCharacter(brain.name).?; + var character = api.findCharacter(brain.name).?; const action_queue = &brain.action_queue; // Deposit items and gold to bank if full @@ -197,14 +520,10 @@ fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool { try action_queue.append(.{ .move = bank_position }); } - for (character.inventory.slots) |slot| { - if (slot.quantity == 0) continue; - - if (slot.id) |item_id| { - try action_queue.append(.{ - .deposit_item = .{ .id = item_id, .quantity = @intCast(slot.quantity) } - }); - } + for (character.inventory.slice()) |slot| { + try action_queue.append(.{ + .deposit_item = .{ .id = slot.id, .quantity = slot.quantity } + }); } return true; @@ -249,12 +568,12 @@ fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Positi try brain.action_queue.append(.{ .gather = {} }); } -fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.ItemIdQuantity) !bool { +fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.Slot) !bool { var character = api.findCharacter(brain.name).?; var has_all_items = true; for (items) |item_quantity| { - const inventory_quantity = character.inventory.getItem(item_quantity.id); + const inventory_quantity = character.inventory.getQuantity(item_quantity.id); if(inventory_quantity < item_quantity.quantity) { has_all_items = false; break; @@ -267,7 +586,7 @@ fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server. } for (items) |item_quantity| { - const inventory_quantity = character.inventory.getItem(item_quantity.id); + const inventory_quantity = character.inventory.getQuantity(item_quantity.id); if(inventory_quantity < item_quantity.quantity) { try brain.action_queue.append(.{ .withdraw_item = .{ .id = item_quantity.id, @@ -282,7 +601,7 @@ fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server. 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); + const inventory_quantity = character.inventory.getQuantity(id); if (inventory_quantity >= quantity) { return false; } @@ -319,7 +638,7 @@ fn craftItem(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: 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); + const inventory_quantity = character.inventory.getQuantity(id); if (inventory_quantity >= quantity) { if (try depositItemsToBank(api, brain)) { return; @@ -340,7 +659,7 @@ fn craftRoutine(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantit needed_item.quantity *= quantity; } - if (try withdrawFromBank(api, brain, needed_items.constSlice())) { + if (try withdrawFromBank(api, brain, needed_items.slice())) { return; } @@ -375,64 +694,89 @@ pub fn main() !void { goal_manager.characters.items[0].routine = .{ .fight = .{ .at = Position.init(0, 1), - .target = undefined + .target = .{ + .id = try api.getItemId("egg"), + .quantity = 1 + } }, }; goal_manager.characters.items[1].routine = .{ .gather = .{ .at = Position.init(-1, 0), - .target = undefined + .target = .{ + .id = try api.getItemId("ash_wood"), + .quantity = 3 + } } }; goal_manager.characters.items[2].routine = .{ .gather = .{ .at = Position.init(2, 0), - .target = undefined + .target = .{ + .id = try api.getItemId("copper_ore"), + .quantity = 3 + } } }; goal_manager.characters.items[3].routine = .{ .gather = .{ .at = Position.init(4, 2), - .target = undefined + .target = .{ + .id = try api.getItemId("gudgeon"), + .quantity = 3 + } } }; goal_manager.characters.items[4].routine = .{ .fight = .{ .at = Position.init(0, 1), - .target = undefined + .target = .{ + .id = try api.getItemId("raw_chicken"), + .quantity = 1 + } }, }; - // goal_manager.characters.items[2].routine = .{ - // .craft = .{ - // .target = .{ - // .quantity = 3, - // .id = try api.getItemId("copper"), - // } - // } - // }; - + const APIError = Server.APIError; std.log.info("Starting main loop", .{}); while (true) { - try goal_manager.runNextAction(); + goal_manager.runNextAction() catch |err| switch (err) { + APIError.ServerUnavailable => { + // If the server is down, wait for a moment and try again. + std.time.sleep(std.time.ns_per_min * 5); + continue; + }, - for (goal_manager.characters.items) |*character| { - if (character.action_queue.items.len > 0) continue; + // TODO: Log all other error to a file or something. So it could be review later on. + else => return err + }; - switch (character.routine) { - .idle => {}, + for (goal_manager.characters.items) |*brain| { + if (brain.action_queue.items.len > 0) continue; + + if (brain.isRoutineFinished()) { + if (!try depositItemsToBank(&api, brain)) { + brain.routine = .idle; + } + continue; + } + + switch (brain.routine) { + .idle => { + std.log.debug("[{s}] idle", .{brain.name}); + }, .fight => |args| { - try fightRoutine(&api, character, args.at); + try fightRoutine(&api, brain, args.at); }, .gather => |args| { - try gatherRoutine(&api, character, args.at); + try gatherRoutine(&api, brain, args.at); }, .craft => |args| { - try craftRoutine(&api, character, args.target.id, args.target.quantity); + try craftRoutine(&api, brain, args.target.id, args.target.quantity); } } }