diff --git a/src/api/server.zig b/src/api/server.zig index 49fa771..e8f0754 100644 --- a/src/api/server.zig +++ b/src/api/server.zig @@ -10,6 +10,7 @@ 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"); +pub const Position = @import("./position.zig"); // Specification: https://api.artifactsmmo.com/docs diff --git a/src/character_brain.zig b/src/character_brain.zig new file mode 100644 index 0000000..aa631dc --- /dev/null +++ b/src/character_brain.zig @@ -0,0 +1,612 @@ +const std = @import("std"); +const Server = @import("./api/server.zig"); +const Allocator = std.mem.Allocator; +const Position = Server.Position; +const assert = std.debug.assert; + +const CharacterBrain = @This(); + +const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically + +pub const QueuedAction = union(enum) { + move: Position, + fight, + gather, + deposit_gold: u64, + deposit_item: Server.ItemIdQuantity, + withdraw_item: Server.ItemIdQuantity, + craft_item: Server.ItemIdQuantity, + + pub fn perform(self: QueuedAction, api: *Server, name: []const u8, ) !QueuedActionResult { + const log = std.log.default; + + switch (self) { + .fight => { + log.debug("[{s}] attack", .{name}); + return .{ + .fight = api.actionFight(name) + }; + }, + .move => |pos| { + log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y}); + return .{ + .move = api.actionMove(name, pos.x, pos.y) + }; + }, + .deposit_gold => |quantity| { + log.debug("[{s}] deposit {} gold", .{name, quantity}); + return .{ + .deposit_gold = api.actionBankDepositGold(name, quantity) + }; + }, + .deposit_item => |item| { + const code = api.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 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 + }; + } +}; + +comptime { + assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len); +} + +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), + +pub 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), + }; +} + +pub fn deinit(self: CharacterBrain) void { + const allocator = self.action_queue.allocator; + allocator.free(self.name); + self.action_queue.deinit(); +} + +fn currentTime() f64 { + const timestamp: f64 = @floatFromInt(std.time.milliTimestamp()); + return timestamp / std.time.ms_per_s; +} + +pub fn performNextAction(self: *CharacterBrain, api: *Server) !void { + const log = std.log.default; + assert(self.action_queue.items.len > 0); + + const APIError = Server.APIError; + + const retry_delay = 0.5; // 500ms + var character = api.findCharacterPtr(self.name).?; + + const next_action = self.action_queue.items[0]; + const action_result = try next_action.perform(api, self.name); + 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 => |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 => |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 => |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 => |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 => |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 => |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); + } + } + } +} + +pub 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; + } + }; +} + +pub fn performRoutine(self: *CharacterBrain, api: *Server) !void { + switch (self.routine) { + .idle => { + std.log.debug("[{s}] idle", .{self.name}); + }, + .fight => |args| { + try self.fightRoutine(api, args.at); + }, + .gather => |args| { + try self.gatherRoutine(api, args.at); + }, + .craft => |args| { + try self.craftRoutine(api, args.target.id, args.target.quantity); + } + } +} + +fn moveIfNeeded(self: *CharacterBrain, api: *Server, pos: Position) !bool { + const character = api.findCharacter(self.name).?; + + if (character.position.eql(pos)) { + return false; + } + + try self.action_queue.append(.{ .move = pos }); + + return true; +} + +pub fn depositItemsToBank(self: *CharacterBrain, api: *Server) !bool { + var character = api.findCharacter(self.name).?; + const action_queue = &self.action_queue; + + // Deposit items and gold to bank if full + if (character.getItemCount() == 0) { + return false; + } + + if (!character.position.eql(bank_position)) { + try action_queue.append(.{ .move = bank_position }); + } + + for (character.inventory.slice()) |slot| { + try action_queue.append(.{ + .deposit_item = .{ .id = slot.id, .quantity = slot.quantity } + }); + } + + return true; +} + +fn depositIfFull(self: *CharacterBrain, api: *Server) !bool { + const character = api.findCharacter(self.name).?; + if (character.getItemCount() < character.inventory_max_items) { + return false; + } + + _ = try self.depositItemsToBank(api); + + if (character.gold > 0) { + try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) }); + } + + return true; +} + +fn fightRoutine(self: *CharacterBrain, api: *Server, enemy_position: Position) !void { + if (try self.depositIfFull(api)) { + return; + } + + if (try self.moveIfNeeded(api, enemy_position)) { + return; + } + + try self.action_queue.append(.{ .fight = {} }); +} + +fn gatherRoutine(self: *CharacterBrain, api: *Server, resource_position: Position) !void { + if (try self.depositIfFull(api)) { + return; + } + + if (try self.moveIfNeeded(api, resource_position)) { + return; + } + + try self.action_queue.append(.{ .gather = {} }); +} + +fn withdrawFromBank(self: *CharacterBrain, api: *Server, items: []const Server.Slot) !bool { + var character = api.findCharacter(self.name).?; + + var has_all_items = true; + for (items) |item_quantity| { + const inventory_quantity = character.inventory.getQuantity(item_quantity.id); + if(inventory_quantity < item_quantity.quantity) { + has_all_items = false; + break; + } + } + if (has_all_items) return false; + + if (try self.moveIfNeeded(api, bank_position)) { + return true; + } + + for (items) |item_quantity| { + const inventory_quantity = character.inventory.getQuantity(item_quantity.id); + if(inventory_quantity < item_quantity.quantity) { + try self.action_queue.append(.{ .withdraw_item = .{ + .id = item_quantity.id, + .quantity = item_quantity.quantity - inventory_quantity, + }}); + } + } + + return true; +} + +fn craftItem(self: *CharacterBrain, api: *Server, id: Server.ItemId, quantity: u64) !bool { + var character = api.findCharacter(self.name).?; + + const inventory_quantity = character.inventory.getQuantity(id); + if (inventory_quantity >= quantity) { + return false; + } + + 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.?; + + // 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 self.moveIfNeeded(api, workstation)) { + return true; + } + + try self.action_queue.append(.{ .craft_item = .{ + .id = id, + .quantity = quantity - inventory_quantity + }}); + + return true; +} + +fn craftRoutine(self: *CharacterBrain, api: *Server, id: Server.ItemId, quantity: u64) !void { + var character = api.findCharacter(self.name).?; + const inventory_quantity = character.inventory.getQuantity(id); + if (inventory_quantity >= quantity) { + if (try self.depositItemsToBank(api)) { + return; + } + } + + 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; + } + + const recipe = target_item.craft.?; + assert(recipe.quantity == 1); // TODO: Add support for recipe which produce multiple items + + var needed_items = recipe.items; + for (needed_items.slice()) |*needed_item| { + needed_item.quantity *= quantity; + } + + if (try self.withdrawFromBank(api, needed_items.slice())) { + return; + } + + if (try self.craftItem(api, id, quantity)) { + return; + } +} diff --git a/src/main.zig b/src/main.zig index 9855539..ee86f85 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,421 +4,10 @@ const assert = std.debug.assert; const Position = @import("./api/position.zig"); const Server = @import("./api/server.zig"); +const CharacterBrain = @import("./character_brain.zig"); // pub const std_options = .{ .log_level = .debug }; -const QueuedAction = union(enum) { - move: Position, - fight, - gather, - deposit_gold: u64, - deposit_item: Server.ItemIdQuantity, - withdraw_item: Server.ItemIdQuantity, - 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) { - 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 deinit(self: CharacterBrain) void { - const allocator = self.action_queue.allocator; - allocator.free(self.name); - self.action_queue.deinit(); - } - - fn performNextAction(self: *CharacterBrain, api: *Server) !void { - const log = std.log.default; - assert(self.action_queue.items.len > 0); - - const APIError = Server.APIError; - - 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 => |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 => |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 => |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 => |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 => |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 => |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; - } - }; - } -}; - fn currentTime() f64 { const timestamp: f64 = @floatFromInt(std.time.milliTimestamp()); return timestamp / std.time.ms_per_s; @@ -477,8 +66,6 @@ const GoalManager = struct { } }; -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); @@ -495,179 +82,6 @@ fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 { 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 brain.action_queue.append(.{ .move = pos }); - - return true; -} - -fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool { - var 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; - } - - if (!character.position.eql(bank_position)) { - try action_queue.append(.{ .move = bank_position }); - } - - for (character.inventory.slice()) |slot| { - try action_queue.append(.{ - .deposit_item = .{ .id = slot.id, .quantity = slot.quantity } - }); - } - - return true; -} - -fn depositIfFull(api: *Server, brain: *CharacterBrain) !bool { - const character = api.findCharacter(brain.name).?; - if (character.getItemCount() < character.inventory_max_items) { - return false; - } - - _ = try depositItemsToBank(api, brain); - - if (character.gold > 0) { - try brain.action_queue.append(.{ .deposit_gold = @intCast(character.gold) }); - } - - return true; -} - -fn fightRoutine(api: *Server, brain: *CharacterBrain, enemy_position: Position) !void { - if (try depositIfFull(api, brain)) { - return; - } - - if (try moveIfNeeded(api, brain, enemy_position)) { - return; - } - - try brain.action_queue.append(.{ .fight = {} }); -} - -fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Position) !void { - if (try depositIfFull(api, brain)) { - return; - } - - if (try moveIfNeeded(api, brain, resource_position)) { - return; - } - - try brain.action_queue.append(.{ .gather = {} }); -} - -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.getQuantity(item_quantity.id); - if(inventory_quantity < item_quantity.quantity) { - has_all_items = false; - break; - } - } - if (has_all_items) return false; - - if (try moveIfNeeded(api, brain, bank_position)) { - return true; - } - - for (items) |item_quantity| { - 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, - .quantity = item_quantity.quantity - inventory_quantity, - }}); - } - } - - return true; -} - -fn craftItem(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !bool { - var character = api.findCharacter(brain.name).?; - - const inventory_quantity = character.inventory.getQuantity(id); - if (inventory_quantity >= quantity) { - return false; - } - - 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.?; - - // 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 brain.action_queue.append(.{ .craft_item = .{ - .id = id, - .quantity = quantity - inventory_quantity - }}); - - return true; -} - -fn craftRoutine(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !void { - var character = api.findCharacter(brain.name).?; - const inventory_quantity = character.inventory.getQuantity(id); - if (inventory_quantity >= quantity) { - if (try depositItemsToBank(api, brain)) { - return; - } - } - - 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; - } - - const recipe = target_item.craft.?; - assert(recipe.quantity == 1); // TODO: Add support for recipe which produce multiple items - - var needed_items = recipe.items; - for (needed_items.slice()) |*needed_item| { - needed_item.quantity *= quantity; - } - - if (try withdrawFromBank(api, brain, needed_items.slice())) { - return; - } - - if (try craftItem(api, brain, id, quantity)) { - return; - } -} - pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -759,26 +173,13 @@ pub fn main() !void { if (brain.action_queue.items.len > 0) continue; if (brain.isRoutineFinished()) { - if (!try depositItemsToBank(&api, brain)) { + if (!try brain.depositItemsToBank(&api)) { brain.routine = .idle; } continue; } - switch (brain.routine) { - .idle => { - std.log.debug("[{s}] idle", .{brain.name}); - }, - .fight => |args| { - try fightRoutine(&api, brain, args.at); - }, - .gather => |args| { - try gatherRoutine(&api, brain, args.at); - }, - .craft => |args| { - try craftRoutine(&api, brain, args.target.id, args.target.quantity); - } - } + try brain.performRoutine(&api); } } }