From dad53513c7eb2fb9974307d579026c9073dc0fbd Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 7 Sep 2024 20:05:15 +0300 Subject: [PATCH] add acceptTask to api --- api/character.zig | 39 +++- api/errors.zig | 194 ++++++++++++++++ api/root.zig | 12 +- api/server.zig | 473 +++++++++++++------------------------- lib/action.zig | 170 ++++++++++++++ lib/brain.zig | 562 ++++------------------------------------------ lib/root.zig | 30 +-- lib/task.zig | 240 +++++++++++++++++++- 8 files changed, 852 insertions(+), 868 deletions(-) create mode 100644 api/errors.zig create mode 100644 lib/action.zig diff --git a/api/character.zig b/api/character.zig index 6bff90d..ca77e8b 100644 --- a/api/character.zig +++ b/api/character.zig @@ -17,6 +17,13 @@ const Inventory = BoundedSlotsArray(20); const Character = @This(); +const TaskMasterTask = struct { + target: []u8, + type: []u8, + progress: u64, + total: u64, +}; + allocator: Allocator, name: []u8, @@ -47,6 +54,8 @@ equipment: Equipment, inventory_max_items: u64, inventory: Inventory, +task: ?TaskMasterTask, + 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; @@ -63,6 +72,27 @@ pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character return error.InvalidInventoryMaxItems; } + var task: ?TaskMasterTask = null; + const task_target = try json_utils.getStringRequired(obj, "task"); + if (task_target.len > 0) { + const progress = try json_utils.getIntegerRequired(obj, "task_progress"); + if (progress < 0) { + return error.InvalidTaskProgress; + } + + const total = try json_utils.getIntegerRequired(obj, "task_total"); + if (total < 0) { + return error.InvalidTaskTotal; + } + + task = TaskMasterTask{ + .target = try allocator.dupe(u8, task_target), + .type = try json_utils.dupeStringRequired(allocator, obj, "task_type"), + .total = @intCast(total), + .progress = @intCast(progress), + }; + } + return Character{ .allocator = allocator, .account = try json_utils.dupeString(allocator, obj, "account"), @@ -92,7 +122,9 @@ pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character .equipment = try Equipment.parse(api, obj), .inventory_max_items = @intCast(inventory_max_items), - .inventory = try Inventory.parse(api, inventory) + .inventory = try Inventory.parse(api, inventory), + + .task = task }; } @@ -100,6 +132,11 @@ pub fn deinit(self: *Character) void { if (self.account) |str| self.allocator.free(str); self.allocator.free(self.name); self.allocator.free(self.skin); + + if (self.task) |task| { + self.allocator.free(task.type); + self.allocator.free(task.target); + } } pub fn getItemCount(self: *const Character) u64 { diff --git a/api/errors.zig b/api/errors.zig new file mode 100644 index 0000000..1f9cf4f --- /dev/null +++ b/api/errors.zig @@ -0,0 +1,194 @@ +const std = @import("std"); + +const ErrorDefinition = struct { + name: [:0]const u8, + code: ?u10, + + fn init(name: [:0]const u8, code: ?u10) ErrorDefinition { + return ErrorDefinition{ + .name = name, + .code = code + }; + } +}; + +fn ErrorDefinitionList(errors: []const ErrorDefinition) type { + var errorNames: [errors.len]std.builtin.Type.Error = undefined; + for (0.., errors) |i, def| { + errorNames[i] = .{ .name = def.name }; + } + + const error_set = @Type(.{ .ErrorSet = &errorNames }); + + return struct { + const ErrorSet = error_set; + + fn parse(status: std.http.Status) ?ErrorSet { + inline for (errors) |err| { + if (err.code == @intFromEnum(status)) { + return @field(ErrorSet, err.name); + } + } + return null; + } + }; +} + +const ServerUnavailable = ErrorDefinition.init("ServerUnavailable", 503); +const RequestFailed = ErrorDefinition.init("RequestFailed", null); +const ParseFailed = ErrorDefinition.init("ParseFailed", null); +const OutOfMemory = ErrorDefinition.init("OutOfMemory", null); + +const MapNotFound = ErrorDefinition.init("MapNotFound", 404); +const ItemNotFound = ErrorDefinition.init("ItemNotFound", 404); +const RecipeNotFound = ErrorDefinition.init("RecipeNotFound", 404); + +const BankIsBusy = ErrorDefinition.init("BankIsBusy", 461); +const NotEnoughItems = ErrorDefinition.init("NotEnoughItems", 478); +const SlotIsFull = ErrorDefinition.init("SlotIsFull", 485); +const CharacterIsBusy = ErrorDefinition.init("CharacterIsBusy", 486); +const AlreadyHasTask = ErrorDefinition.init("AlreadyHasTask", 486); + +const CharacterAtDestination = ErrorDefinition.init("CharacterAtDestination", 490); +const SlotIsEmpty = ErrorDefinition.init("SlotIsEmpty", 491); +const NotEnoughGold = ErrorDefinition.init("NotEnoughGold", 492); +const NotEnoughSkill = ErrorDefinition.init("NotEnoughSkill", 493); +const CharacterIsFull = ErrorDefinition.init("CharacterIsFull", 497); +const CharacterNotFound = ErrorDefinition.init("CharacterNotFound", 498); +const CharacterInCooldown = ErrorDefinition.init("CharacterInCooldown", 499); + +const BankNotFound = ErrorDefinition.init("BankNotFound", 598); +const MonsterNotFound = ErrorDefinition.init("MonsterNotFound", 598); +const ResourceNotFound = ErrorDefinition.init("ResourceNotFound", 598); +const WorkshopNotFound = ErrorDefinition.init("WorkshopNotFound", 598); +const TaskMasterNotFound = ErrorDefinition.init("TaskMasterNotFound", 598); + +pub const FetchError = ErrorDefinitionList(&[_]ErrorDefinition{ + ServerUnavailable, + RequestFailed, + ParseFailed, + OutOfMemory, +}).ErrorSet; + +const MoveErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + MapNotFound, + CharacterIsBusy, + CharacterAtDestination, + CharacterNotFound, + CharacterInCooldown +}); +pub const MoveError = FetchError || MoveErrorDef.ErrorSet; +pub const parseMoveError = MoveErrorDef.parse; + +const FightErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + CharacterIsBusy, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, + MonsterNotFound, +}); +pub const FightError = FetchError || FightErrorDef.ErrorSet; +pub const parseFightError = FightErrorDef.parse; + +const GatherErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + CharacterIsBusy, + NotEnoughSkill, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, + ResourceNotFound +}); +pub const GatherError = FetchError || GatherErrorDef.ErrorSet; +pub const parseGatherError = GatherErrorDef.parse; + +const BankDepositItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + ItemNotFound, + BankIsBusy, + NotEnoughItems, + CharacterIsBusy, + CharacterNotFound, + CharacterInCooldown, + BankNotFound +}); +pub const BankDepositItemError = FetchError || BankDepositItemErrorDef.ErrorSet; +pub const parseBankDepositItemError = BankDepositItemErrorDef.parse; + +const BankDepositGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + BankIsBusy, + NotEnoughGold, + CharacterIsBusy, + CharacterNotFound, + CharacterInCooldown, + BankNotFound +}); +pub const BankDepositGoldError = FetchError || BankDepositGoldErrorDef.ErrorSet; +pub const parseBankDepositGoldError = BankDepositGoldErrorDef.parse; + +const BankWithdrawGoldErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + BankIsBusy, + NotEnoughGold, + CharacterIsBusy, + CharacterNotFound, + CharacterInCooldown, + BankNotFound +}); +pub const BankWithdrawGoldError = FetchError || BankWithdrawGoldErrorDef.ErrorSet; +pub const parseBankWithdrawGoldError = BankWithdrawGoldErrorDef.parse; + +const BankWithdrawItemErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + ItemNotFound, + BankIsBusy, + NotEnoughItems, + CharacterIsBusy, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, + BankNotFound +}); +pub const BankWithdrawItemError = FetchError || BankWithdrawItemErrorDef.ErrorSet; +pub const parseBankWithdrawItemError = BankWithdrawItemErrorDef.parse; + +const CraftErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + RecipeNotFound, + NotEnoughItems, + CharacterIsBusy, + NotEnoughSkill, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, + WorkshopNotFound +}); +pub const CraftError = FetchError || CraftErrorDef.ErrorSet; +pub const parseCraftError = CraftErrorDef.parse; + +const UnequipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + ItemNotFound, // TODO: Can this really occur? maybe a bug in docs + CharacterIsBusy, + SlotIsEmpty, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, +}); +pub const UnequipError = FetchError || UnequipErrorDef.ErrorSet; +pub const parseUnequipError = UnequipErrorDef.parse; + +const EquipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + ItemNotFound, + SlotIsFull, + CharacterIsBusy, + NotEnoughSkill, + CharacterNotFound, + CharacterInCooldown, +}); +pub const EquipError = FetchError || EquipErrorDef.ErrorSet; +pub const parseEquipError = EquipErrorDef.parse; + +const TaskAcceptErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{ + CharacterIsBusy, + AlreadyHasTask, + CharacterNotFound, + CharacterInCooldown, + TaskMasterNotFound +}); +pub const TaskAcceptError = FetchError || TaskAcceptErrorDef.ErrorSet; +pub const parseTaskAcceptError = TaskAcceptErrorDef.parse; diff --git a/api/root.zig b/api/root.zig index 06f3a7f..1865025 100644 --- a/api/root.zig +++ b/api/root.zig @@ -3,6 +3,16 @@ pub const Server = @import("server.zig"); pub const Position = @import("position.zig"); pub const BoundedSlotsArray = @import("slot_array.zig").BoundedSlotsArray; -pub const Error = Server.APIError; +pub const Slot = Server.Slot; pub const ItemId = Server.ItemId; pub const ItemIdQuantity = Server.ItemIdQuantity; + +const errors = @import("errors.zig"); +pub const FetchError = errors.FetchError; +pub const MoveError = errors.MoveError; +pub const FightError = errors.FightError; +pub const GatherError = errors.GatherError; +pub const BankDepositGoldError = errors.BankDepositGoldError; +pub const BankDepositItemError = errors.BankDepositItemError; +pub const BankWithdrawItemError = errors.BankWithdrawItemError; +pub const CraftError = errors.CraftError; diff --git a/api/server.zig b/api/server.zig index fe2a717..c9309e4 100644 --- a/api/server.zig +++ b/api/server.zig @@ -13,6 +13,9 @@ const EnumStringUtils = @import("./enum_string_utils.zig").EnumStringUtils; pub const Slot = @import("./slot.zig"); pub const Position = @import("./position.zig"); +const errors = @import("./errors.zig"); +const FetchError = errors.FetchError; + // Specification: https://api.artifactsmmo.com/docs const Server = @This(); @@ -41,108 +44,6 @@ prefetched_maps: bool = false, prefetched_monsters: bool = false, prefetched_items: bool = false, -// ------------------------- API errors ------------------------ - -pub const APIError = error { - ServerUnavailable, - RequestFailed, - ParseFailed, - OutOfMemory -}; - -pub const MoveError = APIError || error { - MapNotFound, - CharacterIsBusy, - CharacterAtDestination, - CharacterNotFound, - CharacterInCooldown -}; - -pub const FightError = APIError || error { - CharacterIsBusy, - CharacterIsFull, - CharacterNotFound, - CharacterInCooldown, - MonsterNotFound, -}; - -pub const GatherError = APIError || error { - CharacterIsBusy, - NotEnoughSkill, - CharacterIsFull, - CharacterNotFound, - CharacterInCooldown, - ResourceNotFound -}; - -pub const BankDepositItemError = APIError || error { - ItemNotFound, - BankIsBusy, - NotEnoughItems, - CharacterIsBusy, - CharacterNotFound, - CharacterInCooldown, - BankNotFound -}; - -pub const BankDepositGoldError = APIError || error { - BankIsBusy, - NotEnoughGold, - CharacterIsBusy, - CharacterNotFound, - CharacterInCooldown, - BankNotFound -}; - -pub const BankWithdrawGoldError = APIError || error { - BankIsBusy, - NotEnoughGold, - CharacterIsBusy, - CharacterNotFound, - CharacterInCooldown, - BankNotFound -}; - -pub const BankWithdrawItemError = APIError || error { - ItemNotFound, - BankIsBusy, - NotEnoughItems, - CharacterIsBusy, - CharacterIsFull, - CharacterNotFound, - CharacterInCooldown, - BankNotFound -}; - -pub const CraftError = APIError || error { - RecipeNotFound, - NotEnoughItems, - CharacterIsBusy, - NotEnoughSkill, - CharacterIsFull, - CharacterNotFound, - CharacterInCooldown, - WorkshopNotFound -}; - -pub const UnequipError = APIError || error { - ItemNotFound, // TODO: Can this really occur? maybe a bug in docs - CharacterIsBusy, - SlotIsEmpty, - CharacterIsFull, - CharacterNotFound, - CharacterInCooldown, -}; - -pub const EquipError = APIError || error { - ItemNotFound, - SlotIsFull, - CharacterIsBusy, - NotEnoughSkill, - CharacterNotFound, - CharacterInCooldown, -}; - // ------------------------- API result structs ------------------------ pub const EquipmentSlot = @import("./equipment.zig").Slot; @@ -285,17 +186,6 @@ pub const FightResult = struct { .character = try Character.parse(api, character, allocator) }; } - - pub fn parseError(status: std.http.Status) ?FightError { - return switch (@intFromEnum(status)) { - 486 => FightError.CharacterIsBusy, - 497 => FightError.CharacterIsFull, - 498 => FightError.CharacterNotFound, - 499 => FightError.CharacterInCooldown, - 598 => FightError.MonsterNotFound, - else => null - }; - } }; // TODO: Replace this with ItemSlot struct @@ -354,18 +244,6 @@ pub const GatherResult = struct { .character = try Character.parse(api, character, allocator) }; } - - pub fn parseError(status: std.http.Status) ?GatherError { - return switch (@intFromEnum(status)) { - 486 => GatherError.CharacterIsBusy, - 493 => GatherError.NotEnoughSkill, - 497 => GatherError.CharacterIsFull, - 498 => GatherError.CharacterNotFound, - 499 => GatherError.CharacterInCooldown, - 598 => GatherError.ResourceNotFound, - else => null - }; - } }; pub const MoveResult = struct { @@ -381,17 +259,6 @@ pub const MoveResult = struct { .character = try Character.parse(api, character, allocator) }; } - - pub fn parseError(status: std.http.Status) ?MoveError { - return switch (@intFromEnum(status)) { - 404 => MoveError.MapNotFound, - 486 => MoveError.CharacterIsBusy, - 490 => MoveError.CharacterAtDestination, - 498 => MoveError.CharacterNotFound, - 499 => MoveError.CharacterInCooldown, - else => null - }; - } }; pub const GoldTransactionResult = struct { @@ -407,30 +274,6 @@ pub const GoldTransactionResult = struct { .character = try Character.parse(api, character, allocator) }; } - - pub fn parseDepositError(status: std.http.Status) ?BankDepositGoldError { - return switch (@intFromEnum(status)) { - 461 => BankDepositGoldError.BankIsBusy, - 486 => BankDepositGoldError.CharacterIsBusy, - 492 => BankDepositGoldError.NotEnoughGold, - 498 => BankDepositGoldError.CharacterNotFound, - 499 => BankDepositGoldError.CharacterInCooldown, - 598 => BankDepositGoldError.BankNotFound, - else => null - }; - } - - pub fn parseWithdrawError(status: std.http.Status) ?BankWithdrawGoldError { - return switch (@intFromEnum(status)) { - 460 => BankWithdrawGoldError.NotEnoughGold, - 461 => BankWithdrawGoldError.BankIsBusy, - 486 => BankWithdrawGoldError.CharacterIsBusy, - 498 => BankWithdrawGoldError.CharacterNotFound, - 499 => BankWithdrawGoldError.CharacterInCooldown, - 598 => BankWithdrawGoldError.BankNotFound, - else => null - }; - } }; pub const ItemTransactionResult = struct { @@ -446,33 +289,6 @@ pub const ItemTransactionResult = struct { .character = try Character.parse(api, character, allocator) }; } - - pub fn parseDepositError(status: std.http.Status) ?BankDepositItemError { - return switch (@intFromEnum(status)) { - 404 => BankDepositItemError.ItemNotFound, - 461 => BankDepositItemError.BankIsBusy, - 478 => BankDepositItemError.NotEnoughItems, - 486 => BankDepositItemError.CharacterIsBusy, - 498 => BankDepositItemError.CharacterNotFound, - 499 => BankDepositItemError.CharacterInCooldown, - 598 => BankDepositItemError.BankNotFound, - else => null - }; - } - - pub fn parseWithdrawError(status: std.http.Status) ?BankWithdrawItemError { - return switch (@intFromEnum(status)) { - 404 => BankWithdrawItemError.ItemNotFound, - 461 => BankWithdrawItemError.BankIsBusy, - 478 => BankWithdrawItemError.NotEnoughItems, - 486 => BankWithdrawItemError.CharacterIsBusy, - 497 => BankWithdrawItemError.CharacterIsFull, - 498 => BankWithdrawItemError.CharacterNotFound, - 499 => BankWithdrawItemError.CharacterInCooldown, - 598 => BankWithdrawItemError.BankNotFound, - else => null - }; - } }; pub const CraftResult = struct { @@ -491,20 +307,6 @@ pub const CraftResult = struct { .character = try Character.parse(api, character, allocator) }; } - - pub fn parseError(status: std.http.Status) ?CraftError { - return switch (@intFromEnum(status)) { - 404 => CraftError.RecipeNotFound, - 478 => CraftError.NotEnoughItems, - 486 => CraftError.CharacterIsBusy, - 493 => CraftError.NotEnoughSkill, - 497 => CraftError.CharacterIsFull, - 498 => CraftError.CharacterNotFound, - 499 => CraftError.CharacterInCooldown, - 598 => CraftError.WorkshopNotFound, - else => null - }; - } }; pub const UnequipResult = struct { @@ -526,18 +328,6 @@ pub const UnequipResult = struct { .item = item_id }; } - - pub fn parseError(status: std.http.Status) ?UnequipError { - return switch (@intFromEnum(status)) { - 404 => UnequipError.ItemNotFound, - 486 => UnequipError.CharacterIsBusy, - 491 => UnequipError.SlotIsEmpty, - 497 => UnequipError.CharacterIsFull, - 498 => UnequipError.CharacterNotFound, - 499 => UnequipError.CharacterInCooldown, - else => null - }; - } }; pub const EquipResult = struct { @@ -553,18 +343,56 @@ pub const EquipResult = struct { .cooldown = try Cooldown.parse(cooldown) }; } +}; - pub fn parseError(status: std.http.Status) ?EquipError { - return switch (@intFromEnum(status)) { - 404 => EquipError.ItemNotFound, - 478 => EquipError.ItemNotFound, // TODO: What is the difference between 404 and 478? - 485 => EquipError.SlotIsFull, - 486 => EquipError.CharacterIsBusy, - 491 => EquipError.SlotIsFull, // TODO: What is the difference between 485 and 491? - 496 => EquipError.NotEnoughSkill, - 498 => EquipError.CharacterNotFound, - 499 => EquipError.CharacterInCooldown, - else => null +pub const TaskType = enum { + monsters, + resources, + crafts +}; +pub const TaskTypeUtils = EnumStringUtils(TaskType, .{ + .{ "monsters" , TaskType.monsters }, + .{ "resources", TaskType.resources }, + .{ "crafts" , TaskType.crafts }, +}); + +pub const Task = struct { + id: ItemId, // TODO: Refactor `ItemId` to include other object types + type: TaskType, + total: u64, + + pub fn parse(api: *Server, obj: json.ObjectMap) !Task { + const task_type = try json_utils.getStringRequired(obj, "type"); + const total = try json_utils.getIntegerRequired(obj, "total"); + if (total < 0) { + return error.InvalidTaskTotal; + } + + const code = json_utils.getStringRequired(obj, "code"); + const id = try api.getItemId(code); + + return Task{ + .id = id, + .type = TaskTypeUtils.fromString(task_type) orelse return error.InvalidTaskType, + .total = @intCast(total) + }; + } +}; + +pub const AcceptTaskResult = struct { + cooldown: Cooldown, + character: Character, + task: Task, + + pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !AcceptTaskResult { + const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; + const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty; + const task = json_utils.getObject(obj, "task") orelse return error.MissingProperty; + + return EquipResult{ + .cooldown = try Cooldown.parse(cooldown), + .character = try Character.parse(api, character, allocator), + .task = try Task.parse(api, task) }; } }; @@ -578,12 +406,12 @@ pub const MapContentType = enum { tasks_master, }; pub const MapContentTypeUtils = EnumStringUtils(MapContentType, .{ - .{ "monster" , .monster }, - .{ "resource" , .resource }, - .{ "workshop" , .workshop }, - .{ "bank" , .bank }, - .{ "grand_exchange", .grand_exchange }, - .{ "tasks_master" , .tasks_master }, + .{ "monster" , MapContentType.monster }, + .{ "resource" , MapContentType.resource }, + .{ "workshop" , MapContentType.workshop }, + .{ "bank" , MapContentType.bank }, + .{ "grand_exchange", MapContentType.grand_exchange }, + .{ "tasks_master" , MapContentType.tasks_master }, }); pub const MapTile = struct { @@ -1010,7 +838,7 @@ fn allocPaginationParams(allocator: Allocator, page: u64, page_size: ?u64) ![]u8 } // TODO: add retries when hitting a ratelimit -fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult { +fn fetch(self: *Server, options: FetchOptions) FetchError!ArtifactsFetchResult { const method = options.method; const path = options.path; const payload = options.payload; @@ -1062,7 +890,7 @@ fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult { defer if (authorization_header) |str| self.allocator.free(str); if (self.token) |token| { - authorization_header = std.fmt.allocPrint(self.allocator, "Bearer {s}", .{token}) catch return APIError.OutOfMemory; + authorization_header = std.fmt.allocPrint(self.allocator, "Bearer {s}", .{token}) catch return FetchError.OutOfMemory; opts.headers.authorization = .{ .override = authorization_header.? }; } @@ -1072,13 +900,13 @@ fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult { log.debug("fetch {} {s}", .{method, path}); } - const result = self.client.fetch(opts) catch return APIError.RequestFailed; + const result = self.client.fetch(opts) catch return FetchError.RequestFailed; const response_body = response_storage.items; log.debug("fetch result {}", .{result.status}); if (result.status == .service_unavailable) { - return APIError.ServerUnavailable; + return FetchError.ServerUnavailable; } else if (result.status != .ok) { return ArtifactsFetchResult{ .arena = arena, @@ -1086,20 +914,20 @@ fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult { }; } - const parsed = json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed }) catch return APIError.ParseFailed; + const parsed = json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed }) catch return FetchError.ParseFailed; if (parsed != json.Value.object) { - return APIError.ParseFailed; + return FetchError.ParseFailed; } result_status = result.status; if (options.paginated) { - const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return APIError.ParseFailed; - if (total_pages_i64 < 0) return APIError.ParseFailed; + const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return FetchError.ParseFailed; + if (total_pages_i64 < 0) return FetchError.ParseFailed; total_pages = @intCast(total_pages_i64); - const page_results = json_utils.getArray(parsed.object, "data") orelse return APIError.ParseFailed; - fetch_results.appendSlice(page_results.items) catch return APIError.OutOfMemory; + const page_results = json_utils.getArray(parsed.object, "data") orelse return FetchError.ParseFailed; + fetch_results.appendSlice(page_results.items) catch return FetchError.OutOfMemory; if (current_page >= total_pages) break; } else { @@ -1125,7 +953,7 @@ fn handleFetchError( parseError: ?fn (status: std.http.Status) ?Error, ) ?Error { if (status != .ok) { - if (Error != APIError) { + if (Error != FetchError) { if (parseError == null) { @compileError("`parseError` must be defined, if `Error` is not `APIError`"); } @@ -1166,15 +994,15 @@ fn fetchOptionalObject( return null; } if (result.status != .ok) { - return APIError.RequestFailed; + return FetchError.RequestFailed; } if (result.body == null) { - return APIError.ParseFailed; + return FetchError.ParseFailed; } - const body = json_utils.asObject(result.body.?) orelse return APIError.ParseFailed; - return @call(.auto, parseObject, .{ self, body } ++ parseObjectArgs) catch return APIError.ParseFailed; + const body = json_utils.asObject(result.body.?) orelse return FetchError.ParseFailed; + return @call(.auto, parseObject, .{ self, body } ++ parseObjectArgs) catch return FetchError.ParseFailed; } fn fetchObject( @@ -1187,7 +1015,7 @@ fn fetchObject( fetchOptions: FetchOptions ) Error!Object { const result = try self.fetchOptionalObject(Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions); - return result orelse return APIError.RequestFailed; + return result orelse return FetchError.RequestFailed; } fn fetchOptionalArray( @@ -1214,11 +1042,11 @@ fn fetchOptionalArray( return null; } if (result.status != .ok) { - return APIError.RequestFailed; + return FetchError.RequestFailed; } if (result.body == null) { - return APIError.ParseFailed; + return FetchError.ParseFailed; } var array = std.ArrayList(Object).init(allocator); @@ -1233,12 +1061,12 @@ fn fetchOptionalArray( array.deinit(); } - const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed; + const result_data = json_utils.asArray(result.body.?) orelse return FetchError.ParseFailed; for (result_data.items) |result_item| { - const item_obj = json_utils.asObject(result_item) orelse return APIError.ParseFailed; + const item_obj = json_utils.asObject(result_item) orelse return FetchError.ParseFailed; - const parsed_item = @call(.auto, parseObject, .{ self, item_obj } ++ parseObjectArgs) catch return APIError.ParseFailed; - array.append(parsed_item) catch return APIError.OutOfMemory; + const parsed_item = @call(.auto, parseObject, .{ self, item_obj } ++ parseObjectArgs) catch return FetchError.ParseFailed; + array.append(parsed_item) catch return FetchError.OutOfMemory; } return array; @@ -1255,7 +1083,7 @@ fn fetchArray( fetchOptions: FetchOptions ) Error!std.ArrayList(Object) { const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions); - return result orelse return APIError.RequestFailed; + return result orelse return FetchError.RequestFailed; } pub fn setURL(self: *Server, url: []const u8) !void { @@ -1431,9 +1259,9 @@ pub fn prefetchItems(self: *Server) !void { // ------------------------- Endpoints ------------------------ -pub fn getServerStatus(self: *Server) !ServerStatus { +pub fn getServerStatus(self: *Server) FetchError!ServerStatus { return try self.fetchObject( - APIError, + FetchError, null, ServerStatus, ServerStatus.parse, .{ self.allocator }, @@ -1441,12 +1269,12 @@ pub fn getServerStatus(self: *Server) !ServerStatus { ); } -pub fn getCharacter(self: *Server, name: []const u8) APIError!?Character { +pub fn getCharacter(self: *Server, name: []const u8) FetchError!?Character { const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name}); defer self.allocator.free(path); var maybe_character = try self.fetchOptionalObject( - APIError, + FetchError, null, Character, Character.parse, .{ self.allocator }, @@ -1462,10 +1290,10 @@ pub fn getCharacter(self: *Server, name: []const u8) APIError!?Character { } } -pub fn listMyCharacters(self: *Server) APIError!std.ArrayList(Character) { +pub fn listMyCharacters(self: *Server) FetchError!std.ArrayList(Character) { const characters = try self.fetchArray( self.allocator, - APIError, + FetchError, null, Character, Character.parse, .{ self.allocator }, @@ -1479,13 +1307,13 @@ pub fn listMyCharacters(self: *Server) APIError!std.ArrayList(Character) { return characters; } -pub fn actionFight(self: *Server, name: []const u8) FightError!FightResult { +pub fn actionFight(self: *Server, name: []const u8) errors.FightError!FightResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name}); defer self.allocator.free(path); const result = try self.fetchObject( - FightError, - FightResult.parseError, + errors.FightError, + errors.parseFightError, FightResult, FightResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } @@ -1495,13 +1323,13 @@ pub fn actionFight(self: *Server, name: []const u8) FightError!FightResult { return result; } -pub fn actionGather(self: *Server, name: []const u8) GatherError!GatherResult { +pub fn actionGather(self: *Server, name: []const u8) errors.GatherError!GatherResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/gathering", .{name}); defer self.allocator.free(path); const result = try self.fetchObject( - GatherError, - GatherResult.parseError, + errors.GatherError, + errors.parseGatherError, GatherResult, GatherResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path } @@ -1511,7 +1339,7 @@ pub fn actionGather(self: *Server, name: []const u8) GatherError!GatherResult { return result; } -pub fn actionMove(self: *Server, name: []const u8, x: i64, y: i64) MoveError!MoveResult { +pub fn actionMove(self: *Server, name: []const u8, x: i64, y: i64) errors.MoveError!MoveResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/move", .{name}); defer self.allocator.free(path); @@ -1519,8 +1347,8 @@ pub fn actionMove(self: *Server, name: []const u8, x: i64, y: i64) MoveError!Mov defer self.allocator.free(payload); const result = try self.fetchObject( - MoveError, - MoveResult.parseError, + errors.MoveError, + errors.parseMoveError, MoveResult, MoveResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1534,7 +1362,7 @@ pub fn actionBankDepositGold( self: *Server, name: []const u8, quantity: u64 -) BankDepositGoldError!GoldTransactionResult { +) errors.BankDepositGoldError!GoldTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit/gold", .{name}); defer self.allocator.free(path); @@ -1542,8 +1370,8 @@ pub fn actionBankDepositGold( defer self.allocator.free(payload); const result = try self.fetchObject( - BankDepositGoldError, - GoldTransactionResult.parseDepositError, + errors.BankDepositGoldError, + errors.parseBankDepositGoldError, GoldTransactionResult, GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1558,7 +1386,7 @@ pub fn actionBankDepositItem( name: []const u8, code: []const u8, quantity: u64 -) BankDepositItemError!ItemTransactionResult { +) errors.BankDepositItemError!ItemTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit", .{name}); defer self.allocator.free(path); @@ -1566,8 +1394,8 @@ pub fn actionBankDepositItem( defer self.allocator.free(payload); const result = try self.fetchObject( - BankDepositItemError, - ItemTransactionResult.parseDepositError, + errors.BankDepositItemError, + errors.parseBankDepositItemError, ItemTransactionResult, ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1581,7 +1409,7 @@ pub fn actionBankWithdrawGold( self: *Server, name: []const u8, quantity: u64 -) BankDepositGoldError!GoldTransactionResult { +) errors.BankDepositGoldError!GoldTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/withdraw/gold", .{name}); defer self.allocator.free(path); @@ -1589,8 +1417,8 @@ pub fn actionBankWithdrawGold( defer self.allocator.free(payload); const result = try self.fetchObject( - BankWithdrawGoldError, - GoldTransactionResult.parseWithdrawError, + errors.BankWithdrawGoldError, + errors.parseBankWithdrawGoldError, GoldTransactionResult, GoldTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1605,7 +1433,7 @@ pub fn actionBankWithdrawItem( name: []const u8, code: []const u8, quantity: u64 -) BankWithdrawItemError!ItemTransactionResult { +) errors.BankWithdrawItemError!ItemTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/withdraw", .{name}); defer self.allocator.free(path); @@ -1613,8 +1441,8 @@ pub fn actionBankWithdrawItem( defer self.allocator.free(payload); const result = try self.fetchObject( - BankWithdrawItemError, - ItemTransactionResult.parseWithdrawError, + errors.BankWithdrawItemError, + errors.parseBankWithdrawItemError, ItemTransactionResult, ItemTransactionResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1637,8 +1465,8 @@ pub fn actionCraft( defer self.allocator.free(payload); const result = try self.fetchObject( - CraftError, - CraftResult.parseError, + errors.CraftError, + errors.parseCraftError, CraftResult, CraftResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1652,7 +1480,7 @@ pub fn actionUnequip( self: *Server, name: []const u8, slot: EquipmentSlot -) UnequipError!UnequipResult { +) errors.UnequipError!UnequipResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/unequip", .{name}); defer self.allocator.free(path); @@ -1660,8 +1488,8 @@ pub fn actionUnequip( defer self.allocator.free(payload); const result = try self.fetchObject( - UnequipError, - UnequipResult.parseError, + errors.UnequipError, + errors.parseUnequipError, UnequipResult, UnequipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1676,7 +1504,7 @@ pub fn actionEquip( name: []const u8, slot: EquipmentSlot, code: []const u8 -) EquipError!EquipResult { +) errors.EquipError!EquipResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/equip", .{name}); defer self.allocator.free(path); @@ -1684,8 +1512,8 @@ pub fn actionEquip( defer self.allocator.free(payload); const result = try self.fetchObject( - EquipError, - EquipResult.parseError, + errors.EquipError, + errors.parseEquipError, EquipResult, EquipResult.parse, .{ self.allocator }, .{ .method = .POST, .path = path, .payload = payload } @@ -1695,28 +1523,28 @@ pub fn actionEquip( return result; } -pub fn getBankGold(self: *Server) APIError!u64 { +pub fn getBankGold(self: *Server) FetchError!u64 { const result = try self.fetch(.{ .method = .GET, .path = "/my/bank/gold" }); defer result.deinit(); if (result.status != .ok) { - return APIError.RequestFailed; + return FetchError.RequestFailed; } if (result.body == null) { - return APIError.ParseFailed; + return FetchError.ParseFailed; } - const data = json_utils.asObject(result.body.?) orelse return APIError.RequestFailed; - const quantity = json_utils.getInteger(data, "quantity") orelse return APIError.ParseFailed; - if (quantity < 0) return APIError.ParseFailed; + const data = json_utils.asObject(result.body.?) orelse return FetchError.RequestFailed; + const quantity = json_utils.getInteger(data, "quantity") orelse return FetchError.ParseFailed; + if (quantity < 0) return FetchError.ParseFailed; return @intCast(quantity); } -pub fn getBankItems(self: *Server, allocator: Allocator) APIError!std.ArrayList(ItemIdQuantity) { +pub fn getBankItems(self: *Server, allocator: Allocator) FetchError!std.ArrayList(ItemIdQuantity) { return self.fetchArray( allocator, - APIError, + FetchError, null, ItemIdQuantity, ItemIdQuantity.parse, .{}, @@ -1724,13 +1552,13 @@ pub fn getBankItems(self: *Server, allocator: Allocator) APIError!std.ArrayList( ); } -pub fn getBankItemQuantity(self: *Server, code: []const u8) APIError!?u64 { +pub fn getBankItemQuantity(self: *Server, code: []const u8) FetchError!?u64 { const query = try std.fmt.allocPrint(self.allocator, "item_code={s}", .{code}); defer self.allocator.free(query); const maybe_items = try self.fetchOptionalArray( self.allocator, - APIError, + FetchError, null, ItemIdQuantity, ItemIdQuantity.parse, .{}, @@ -1750,7 +1578,7 @@ pub fn getBankItemQuantity(self: *Server, code: []const u8) APIError!?u64 { return list_items[0].quantity; } -pub fn getMap(self: *Server, x: i64, y: i64) APIError!?MapTile { +pub fn getMap(self: *Server, x: i64, y: i64) FetchError!?MapTile { const position = Position.init(x, y); if (self.findMap(position)) |map| { @@ -1765,7 +1593,7 @@ pub fn getMap(self: *Server, x: i64, y: i64) APIError!?MapTile { defer self.allocator.free(path); const result = self.fetchOptionalObject( - APIError, + FetchError, null, MapTile, MapTile.parse, .{ self.allocator }, @@ -1784,7 +1612,7 @@ pub const MapOptions = struct { type: ?MapContentType = null, }; -pub fn getMaps(self: *Server, opts: MapOptions) APIError!std.ArrayList(MapTile) { +pub fn getMaps(self: *Server, opts: MapOptions) FetchError!std.ArrayList(MapTile) { if (self.prefetched_maps) { var found = std.ArrayList(MapTile).init(self.allocator); var mapIter = self.maps.valueIterator(); @@ -1815,7 +1643,7 @@ pub fn getMaps(self: *Server, opts: MapOptions) APIError!std.ArrayList(MapTile) const result = try self.fetchArray( self.allocator, - APIError, + FetchError, null, MapTile, MapTile.parse, .{ self.allocator }, @@ -1829,7 +1657,7 @@ pub fn getMaps(self: *Server, opts: MapOptions) APIError!std.ArrayList(MapTile) return result; } -pub fn getItem(self: *Server, code: []const u8) APIError!?Item { +pub fn getItem(self: *Server, code: []const u8) FetchError!?Item { if (self.items.get(code)) |item| { return item; } @@ -1842,7 +1670,7 @@ pub fn getItem(self: *Server, code: []const u8) APIError!?Item { defer self.allocator.free(path); const result = try self.fetchOptionalObject( - APIError, + FetchError, null, ItemWithGE, ItemWithGE.parse, .{ self.allocator }, @@ -1858,7 +1686,7 @@ pub fn getItem(self: *Server, code: []const u8) APIError!?Item { } } -pub fn getItemById(self: *Server, id: ItemId) APIError!?Item { +pub fn getItemById(self: *Server, id: ItemId) FetchError!?Item { const code = self.getItemCode(id) orelse return null; return self.getItem(code); } @@ -1872,7 +1700,7 @@ pub const ItemOptions = struct { type: ?ItemType = null, }; -pub fn getItems(self: *Server, opts: ItemOptions) APIError!std.ArrayList(Item) { +pub fn getItems(self: *Server, opts: ItemOptions) FetchError!std.ArrayList(Item) { if (self.prefetched_items) { var found = std.ArrayList(Item).init(self.allocator); var itemIter = self.items.valueIterator(); @@ -1933,7 +1761,7 @@ pub fn getItems(self: *Server, opts: ItemOptions) APIError!std.ArrayList(Item) { const result = try self.fetchArray( self.allocator, - APIError, + FetchError, null, Item, Item.parse, .{ self.allocator }, @@ -1948,7 +1776,7 @@ pub fn getItems(self: *Server, opts: ItemOptions) APIError!std.ArrayList(Item) { return result; } -pub fn getResource(self: *Server, code: []const u8) APIError!?Resource { +pub fn getResource(self: *Server, code: []const u8) FetchError!?Resource { if (self.resources.get(code)) |resource| { return resource; } @@ -1961,7 +1789,7 @@ pub fn getResource(self: *Server, code: []const u8) APIError!?Resource { defer self.allocator.free(path); const result = try self.fetchOptionalObject( - APIError, + FetchError, null, Resource, Resource.parse, .{ self.allocator }, @@ -1982,7 +1810,7 @@ pub const ResourceOptions = struct { skill: ?ResourceSkill = null, }; -pub fn getResources(self: *Server, opts: ResourceOptions) APIError!std.ArrayList(Resource) { +pub fn getResources(self: *Server, opts: ResourceOptions) FetchError!std.ArrayList(Resource) { if (self.prefetched_resources) { var found = std.ArrayList(Resource).init(self.allocator); var resourceIter = self.resources.valueIterator(); @@ -2028,7 +1856,7 @@ pub fn getResources(self: *Server, opts: ResourceOptions) APIError!std.ArrayList const result = try self.fetchArray( self.allocator, - APIError, + FetchError, null, Resource, Resource.parse, .{ self.allocator }, @@ -2043,7 +1871,7 @@ pub fn getResources(self: *Server, opts: ResourceOptions) APIError!std.ArrayList return result; } -pub fn getMonster(self: *Server, code: []const u8) APIError!?Monster { +pub fn getMonster(self: *Server, code: []const u8) FetchError!?Monster { if (self.monsters.get(code)) |monster| { return monster; } @@ -2056,7 +1884,7 @@ pub fn getMonster(self: *Server, code: []const u8) APIError!?Monster { defer self.allocator.free(path); const result = try self.fetchOptionalObject( - APIError, + FetchError, null, Monster, Monster.parse, .{ self.allocator }, @@ -2076,7 +1904,7 @@ pub const MonsterOptions = struct { min_level: ?u64 = null, }; -pub fn getMonsters(self: *Server, opts: MonsterOptions) APIError!std.ArrayList(Monster) { +pub fn getMonsters(self: *Server, opts: MonsterOptions) FetchError!std.ArrayList(Monster) { if (self.prefetched_monsters) { var found = std.ArrayList(Monster).init(self.allocator); var monsterIter = self.monsters.valueIterator(); @@ -2119,7 +1947,7 @@ pub fn getMonsters(self: *Server, opts: MonsterOptions) APIError!std.ArrayList(M const result = try self.fetchArray( self.allocator, - APIError, + FetchError, null, Monster, Monster.parse, .{ self.allocator }, @@ -2133,3 +1961,18 @@ pub fn getMonsters(self: *Server, opts: MonsterOptions) APIError!std.ArrayList(M return result; } +pub fn acceptTask(self: *Server, name: []const u8) errors.TaskAcceptError { + const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/task/new", .{name}); + defer self.allocator.free(path); + + const result = try self.fetchObject( + errors.TaskAcceptError, + errors.parseTaskAcceptError, + EquipResult, + EquipResult.parse, .{ self.allocator }, + .{ .method = .POST, .path = path } + ); + try self.addOrUpdateCharacter(result.character); + + return result; +} diff --git a/lib/action.zig b/lib/action.zig new file mode 100644 index 0000000..bb14083 --- /dev/null +++ b/lib/action.zig @@ -0,0 +1,170 @@ +const std = @import("std"); +const Api = @import("artifacts-api"); +const Position = Api.Position; +const Server = Api.Server; +const assert = std.debug.assert; + +pub const Action = 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: Action, api: *Server, name: []const u8) !ActionResult { + 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) + }; + } + } + } +}; + +pub const ErrorResponse = enum { + /// Something went wrong, and you probably can't reasonbly recover from it. Bail, bail! + abort, + + /// You probably were trying to an action a bit too early, just try again a bit later. + retry, + + /// Something in your logic went wrong, re-evaluate your state and do something different. + restart, + + /// The error can be safe ignored, continue doing the next action that you wanted. + ignore +}; + +pub const ActionResult = union(enum) { + move: Api.MoveError!Server.MoveResult, + fight: Api.FightError!Server.FightResult, + gather: Api.GatherError!Server.GatherResult, + deposit_gold: Api.BankDepositGoldError!Server.GoldTransactionResult, + deposit_item: Api.BankDepositItemError!Server.ItemTransactionResult, + withdraw_item: Api.BankWithdrawItemError!Server.ItemTransactionResult, + craft_item: Api.CraftError!Server.CraftResult, + + const AnyError = Server.MoveError; + + const Tag = @typeInfo(ActionResult).Union.tag_type.?; + + fn fieldType(comptime kind: Tag) type { + const field_type = std.meta.fields(ActionResult)[@intFromEnum(kind)].type; + return @typeInfo(field_type).ErrorUnion.payload; + } + + pub fn get(self: ActionResult, comptime kind: Tag) ?fieldType(kind) { + return switch (self) { + kind => |v| v catch null, + else => null + }; + } + + pub fn getError(self: ActionResult) !void { + switch (self) { + .fight => |result| { + _ = try result; + }, + .move => |result| { + _ = try result; + }, + .deposit_gold => |result| { + _ = try result; + }, + .deposit_item => |result| { + _ = try result; + }, + .withdraw_item => |result| { + _ = try result; + }, + .gather => |result| { + _ = try result; + }, + .craft_item => |result| { + _ = try result; + } + } + } + + pub fn getErrorResponse(self: ActionResult) ?ErrorResponse { + self.getError() catch |err| switch (err) { + error.CharacterIsBusy, + error.CharacterInCooldown, + error.BankIsBusy => return ErrorResponse.retry, + + error.CharacterAtDestination => return ErrorResponse.ignore, + + error.MapNotFound, + error.CharacterIsFull, + error.MonsterNotFound, + error.NotEnoughSkill, + error.ResourceNotFound, + error.NotEnoughGold, + error.BankNotFound, + error.ItemNotFound, + error.NotEnoughItems, + error.RecipeNotFound, + error.WorkshopNotFound => return ErrorResponse.restart, + + error.CharacterNotFound, + error.ServerUnavailable, + error.RequestFailed, + error.ParseFailed, + error.OutOfMemory => return ErrorResponse.abort + }; + + return null; + } +}; + +comptime { + const ActionTag = @typeInfo(Action).Union.tag_type.?; + const ResultTag = @typeInfo(ActionResult).Union.tag_type.?; + + assert(std.meta.fields(ActionTag).len == std.meta.fields(ResultTag).len); +} diff --git a/lib/brain.zig b/lib/brain.zig index 205b260..86bb76d 100644 --- a/lib/brain.zig +++ b/lib/brain.zig @@ -1,118 +1,28 @@ const std = @import("std"); -const ArtifactsAPI = @import("artifacts-api"); -const Server = ArtifactsAPI.Server; +const Api = @import("artifacts-api"); +const Server = Api.Server; +const Position = Api.Position; const Allocator = std.mem.Allocator; -const Position = Server.Position; const assert = std.debug.assert; const CharacterTask = @import("./task.zig").Task; +const QueuedAction = @import("./action.zig").Action; +const QueuedActionResult = @import("./action.zig").ActionResult; -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); -} +const Brain = @This(); name: []const u8, action_queue: std.ArrayList(QueuedAction), -task: ?*CharacterTask = null, +task: ?CharacterTask = null, -pub fn init(allocator: Allocator, name: []const u8) !CharacterBrain { - return CharacterBrain{ +pub fn init(allocator: Allocator, name: []const u8) !Brain { + return Brain{ .name = try allocator.dupe(u8, name), .action_queue = std.ArrayList(QueuedAction).init(allocator), }; } -pub fn deinit(self: CharacterBrain) void { +pub fn deinit(self: Brain) void { const allocator = self.action_queue.allocator; allocator.free(self.name); self.action_queue.deinit(); @@ -123,450 +33,56 @@ fn currentTime() f64 { return timestamp / std.time.ms_per_s; } -pub fn performNextAction(self: *CharacterBrain, api: *Server) !void { +pub fn performNextAction(self: *Brain, 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; - }, + if (action_result.getErrorResponse()) |error_response| { + switch (error_response) { + .retry => { + character.cooldown_expiration = currentTime() + retry_delay; + log.warn("[{s}] retry withdrawing item", .{self.name}); + }, + .restart => { + log.warn("[{s}] clear action queue", .{self.name}); + self.action_queue.clearAndFree(); + }, + .abort => { + try action_result.getError(); - 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; - } - }; + // The error above should always return + unreachable; + }, + .ignore => { }, } } _ = self.action_queue.orderedRemove(0); - self.onActionCompleted(action_result); -} - -fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void { - if (self.task == null) return; - - switch (self.task.?.*) { - .fight => |*args| { - if (result.get(.fight)) |r| { - const fight_result: Server.FightResult = r; - const drops = fight_result.fight.drops; - - args.progress += drops.getQuantity(args.until.item.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.until.item.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); - } - } + if (self.task) |*task| { + task.onActionCompleted(action_result); } } -pub fn isTaskFinished(self: *CharacterBrain) bool { - if (self.task == null) { - return false; - } +pub fn step(self: *Brain, api: *Api.Server) !void { + if (self.action_queue.items.len > 0) return; - return self.task.?.isComplete(); -} - -pub fn performTask(self: *CharacterBrain, api: *Server) !void { - if (self.task == null) { - // TODO: std.log.debug("[{s}] idle", .{self.name}); - return; - } - - switch (self.task.?.*) { - .fight => |args| { - try self.fightRoutine(api, args.at); - }, - .gather => |args| { - try self.gatherRoutine(api, args.at); - }, - .craft => |args| { - try self.craftRoutine(api, args.at, 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, - }}); + if (self.task) |task| { + if (task.isComplete()) { + // if (try brain.depositItemsToBank(&self.server)) { + // continue; + // } + self.task = null; } } - return true; -} - -fn craftItem(self: *CharacterBrain, api: *Server, workstation: Position, 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; - } - - 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, workstation: Position, 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.?; - - 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, workstation, id, quantity)) { - return; + if (self.task) |task| { + try task.queueActions(api, self.name, &self.action_queue); } } diff --git a/lib/root.zig b/lib/root.zig index 304909d..59ac1ee 100644 --- a/lib/root.zig +++ b/lib/root.zig @@ -3,6 +3,7 @@ const Api = @import("artifacts-api"); const Allocator = std.mem.Allocator; pub const Brain = @import("./brain.zig"); +pub const TaskGraph = @import("./task_graph.zig"); const Artificer = @This(); @@ -11,6 +12,7 @@ const server_down_retry_interval = 5; // minutes server: Api.Server, characters: std.ArrayList(Brain), +task_graph: TaskGraph, paused_until: ?i64 = null, @@ -32,7 +34,8 @@ pub fn init(allocator: Allocator, token: []const u8) !Artificer { return Artificer{ .server = server, - .characters = characters + .characters = characters, + .task_graph = TaskGraph.init(allocator) }; } @@ -53,7 +56,7 @@ pub fn step(self: *Artificer) !void { } runNextActions(self.characters.items, &self.server) catch |err| switch (err) { - Api.Error.ServerUnavailable => { + Api.FetchError.ServerUnavailable => { self.paused_until = std.time.timestamp() + std.time.ns_per_min * server_down_retry_interval; std.log.warn("Server is down, retrying in {}min", .{ server_down_retry_interval }); return; @@ -62,26 +65,15 @@ pub fn step(self: *Artificer) !void { }; for (self.characters.items) |*brain| { - if (brain.action_queue.items.len > 0) continue; - - if (brain.isTaskFinished()) { - if (try brain.depositItemsToBank(&self.server)) { - continue; - } - brain.task = null; - } - if (brain.task == null) { - // var next_tasks = try task_tree.listNextTasks(); - // defer next_tasks.deinit(); - // - // if (next_tasks.items.len > 0) { - // const next_task_index = random.intRangeLessThan(usize, 0, next_tasks.items.len); - // brain.task = next_tasks.items[next_task_index]; - // } + const character = self.server.findCharacter(brain.name).?; + if (character.task == null) { + brain.task = .{ .accept_task = .{} }; + } } - try brain.performTask(&self.server); + + try brain.step(&self.server); } } diff --git a/lib/task.zig b/lib/task.zig index 33e648c..dadb1e9 100644 --- a/lib/task.zig +++ b/lib/task.zig @@ -1,6 +1,14 @@ +const std = @import("std"); const Api = @import("artifacts-api"); const Position = Api.Position; +const Action = @import("./action.zig").Action; +const ActionResult = @import("./action.zig").ActionResult; +const ItemId = Api.ItemId; +const Slot = Api.Slot; + +const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically + pub const UntilCondition = union(enum) { xp: u64, item: Api.ItemIdQuantity, @@ -22,18 +30,232 @@ pub const Task = union(enum) { target: Api.ItemIdQuantity, progress: u64 = 0, }, + accept_task: struct { + done: bool = false + }, pub fn isComplete(self: Task) bool { return switch (self) { - .fight => |args| { - return args.progress >= args.until.item.quantity; - }, - .gather => |args| { - return args.progress >= args.until.item.quantity; - }, - .craft => |args| { - return args.progress >= args.target.quantity; - } + .fight => |args| args.progress >= args.until.item.quantity, + .gather => |args| args.progress >= args.until.item.quantity, + .craft => |args| args.progress >= args.target.quantity, + .accept_task => |args| args.done }; } + + pub fn onActionCompleted(self: *Task, result: ActionResult) void { + switch (self.*) { + .fight => |*args| { + if (result.get(.fight)) |r| { + const fight_result: Api.Server.FightResult = r; + const drops = fight_result.fight.drops; + + args.progress += drops.getQuantity(args.until.item.id); + } + }, + .gather => |*args| { + if (result.get(.gather)) |r| { + const gather_resutl: Api.Server.GatherResult = r; + const items = gather_resutl.details.items; + + args.progress += items.getQuantity(args.until.item.id); + } + }, + .craft => |*args| { + if (result.get(.craft_item)) |r| { + const craft_result: Api.Server.CraftResult = r; + const items = craft_result.details.items; + + args.progress += items.getQuantity(args.target.id); + } + }, + .accept_task => { + // TODO: + } + } + } + + pub fn queueActions(self: Task, api: *Api.Server, name: []const u8, action_queue: *std.ArrayList(Action)) !void { + const ctx = TaskContext{ + .api = api, + .name = name, + .action_queue = action_queue + }; + + switch (self) { + .fight => |args| { + try ctx.fightRoutine(args.at); + }, + .gather => |args| { + try ctx.gatherRoutine(args.at); + }, + .craft => |args| { + try ctx.craftRoutine(args.at, args.target.id, args.target.quantity); + }, + .accept_task => { + // TODO: + } + } + } }; + +const TaskContext = struct { + api: *Api.Server, + name: []const u8, + action_queue: *std.ArrayList(Action), + + fn moveIfNeeded(self: TaskContext, pos: Position) !bool { + const character = self.api.findCharacter(self.name).?; + + if (character.position.eql(pos)) { + return false; + } + + try self.action_queue.append(.{ .move = pos }); + + return true; + } + + pub fn depositItemsToBank(self: TaskContext) !bool { + var character = self.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: TaskContext) !bool { + const character = self.api.findCharacter(self.name).?; + if (character.getItemCount() < character.inventory_max_items) { + return false; + } + + _ = try depositItemsToBank(self); + + if (character.gold > 0) { + try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) }); + } + + return true; + } + + fn fightRoutine(self: TaskContext, enemy_position: Position) !void { + if (try self.depositIfFull()) { + return; + } + + if (try self.moveIfNeeded(enemy_position)) { + return; + } + + try self.action_queue.append(.{ .fight = {} }); + } + + fn gatherRoutine(self: TaskContext, resource_position: Position) !void { + if (try self.depositIfFull()) { + return; + } + + if (try self.moveIfNeeded(resource_position)) { + return; + } + + try self.action_queue.append(.{ .gather = {} }); + } + + fn withdrawFromBank(self: TaskContext, items: []const Slot) !bool { + var character = self.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(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: TaskContext, workstation: Position, id: ItemId, quantity: u64) !bool { + var character = self.api.findCharacter(self.name).?; + + const inventory_quantity = character.inventory.getQuantity(id); + if (inventory_quantity >= quantity) { + return false; + } + + if (try self.moveIfNeeded(workstation)) { + return true; + } + + try self.action_queue.append(.{ .craft_item = .{ + .id = id, + .quantity = quantity - inventory_quantity + }}); + + return true; + } + + fn craftRoutine(self: TaskContext, workstation: Position, id: ItemId, quantity: u64) !void { + var character = self.api.findCharacter(self.name).?; + const inventory_quantity = character.inventory.getQuantity(id); + if (inventory_quantity >= quantity) { + if (try self.depositItemsToBank()) { + return; + } + } + + const code = self.api.getItemCode(id) orelse return error.InvalidItemId; + const target_item = try self.api.getItem(code) orelse return error.ItemNotFound; + if (target_item.craft == null) { + return error.NotCraftable; + } + + const recipe = target_item.craft.?; + + var needed_items = recipe.items; + for (needed_items.slice()) |*needed_item| { + needed_item.quantity *= quantity; + } + + if (try self.withdrawFromBank(needed_items.slice())) { + return; + } + + if (try self.craftItem(workstation, id, quantity)) { + return; + } + } +}; +