diff --git a/src/api/server.zig b/src/api/server.zig index b70b3af..ba6c0c3 100644 --- a/src/api/server.zig +++ b/src/api/server.zig @@ -1088,7 +1088,7 @@ fn fetch(self: *Server, options: FetchOptions) APIError!ArtifactsFetchResult { if (options.paginated) { const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return APIError.ParseFailed; - if (total_pages_i64 < 1) return APIError.ParseFailed; + if (total_pages_i64 < 0) return APIError.ParseFailed; total_pages = @intCast(total_pages_i64); const page_results = json_utils.getArray(parsed.object, "data") orelse return APIError.ParseFailed; diff --git a/src/api/slot_array.zig b/src/api/slot_array.zig index 772283c..fbb35ae 100644 --- a/src/api/slot_array.zig +++ b/src/api/slot_array.zig @@ -13,7 +13,7 @@ pub fn BoundedSlotsArray(comptime slot_count: u32) type { return struct { slots: Slots, - fn init() @This() { + pub fn init() @This() { return @This(){ .slots = Slots.init(0) catch unreachable }; @@ -62,20 +62,13 @@ pub fn BoundedSlotsArray(comptime slot_count: u32) type { } } - pub fn add(self: *@This(), id: ItemId, quantity: u64) void { + pub fn add(self: *@This(), id: ItemId, quantity: u64) !void { + if (quantity == 0) return; + if (self.findSlot(id)) |slot| { slot.quantity += quantity; } else { - var empty_slot: ?*Slot = null; - for (&self.slots) |*slot| { - if (slot.id == null) { - empty_slot = slot; - } - } - - assert(empty_slot != null); - empty_slot.?.id = id; - empty_slot.?.quantity = quantity; + try self.slots.append(Slot{ .id = id, .quantity = quantity }); } } diff --git a/src/character_brain.zig b/src/character_brain.zig index 05898db..62dbe1c 100644 --- a/src/character_brain.zig +++ b/src/character_brain.zig @@ -132,7 +132,7 @@ pub const CharacterTask = union(enum) { 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{ @@ -391,7 +391,7 @@ pub fn performNextAction(self: *CharacterBrain, api: *Server) !void { fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void { if (self.task == null) return; - switch (self.task.?) { + switch (self.task.?.*) { .fight => |*args| { if (result.get(.fight)) |r| { const fight_result: Server.FightResult = r; @@ -433,7 +433,7 @@ pub fn performTask(self: *CharacterBrain, api: *Server) !void { return; } - switch (self.task.?) { + switch (self.task.?.*) { .fight => |args| { try self.fightRoutine(api, args.at); }, diff --git a/src/main.zig b/src/main.zig index e7ebaac..f826514 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,6 +5,7 @@ const assert = std.debug.assert; const Position = @import("./api/position.zig"); const Server = @import("./api/server.zig"); const CharacterBrain = @import("./character_brain.zig"); +const BoundedSlotsArray = @import("./api/slot_array.zig").BoundedSlotsArray; // pub const std_options = .{ .log_level = .debug }; @@ -90,6 +91,7 @@ const TaskTree = struct { const TaskTreeNode = struct { parent: ?usize, character_task: CharacterBrain.CharacterTask, + additional_items: BoundedSlotsArray(8) = BoundedSlotsArray(8).init(), }; const Nodes = std.ArrayList(TaskTreeNode); @@ -115,8 +117,8 @@ const TaskTree = struct { return self.nodes.items.len-1; } - fn appendSubTree(self: *TaskTree, code: []const u8, quantity: u64, parent: ?usize) !void { - if (quantity == 0) return; + fn appendSubTree(self: *TaskTree, code: []const u8, quantity: u64, parent: ?usize) !?usize { + if (quantity == 0) return null; const item = (try self.api.getItem(code)).?; const item_id = try self.api.getItemId(code); @@ -144,62 +146,72 @@ const TaskTree = struct { for (recipe.items.slots.constSlice()) |recipe_item| { const recipe_item_code = self.api.getItemCode(recipe_item.id).?; - try self.appendSubTree(recipe_item_code, recipe_item.quantity * craft_count, node_id); - } - } else { - if (item.type == .resource) { - if (eql(u8, item.subtype, "mining")) { - const resources = try self.api.getResources(.{ .drop = code }); - defer resources.deinit(); + const needed_quantity = recipe_item.quantity * craft_count; + const subtree_id = try self.appendSubTree(recipe_item_code, needed_quantity, node_id); - if (resources.items.len == 0) return error.ResourceNotFound; - if (resources.items.len > 1) std.log.warn("Multiple resources exist for target item", .{}); - const resource_code = resources.items[0].code; - - const resource_maps = try self.api.getMaps(.{ .code = resource_code }); - defer resource_maps.deinit(); - - if (resource_maps.items.len == 0) return error.MapNotFound; - if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for resource", .{}); - const resource_map = resource_maps.items[0]; - - _ = try self.appendNode(TaskTreeNode{ - .parent = parent, - .character_task = .{ - .gather = .{ - .at = resource_map.position, - .target = .{ .id = item_id, .quantity = quantity } - } - } - }); - } else if (eql(u8, item.subtype, "mob")) { - const monsters = try self.api.getMonsters(.{ .drop = code }); - defer monsters.deinit(); - - if (monsters.items.len == 0) return error.ResourceNotFound; - if (monsters.items.len > 1) std.log.warn("Multiple monsters exist for target item", .{}); - const monster_code = monsters.items[0].code; - - const resource_maps = try self.api.getMaps(.{ .code = monster_code }); - defer resource_maps.deinit(); - - if (resource_maps.items.len == 0) return error.MapNotFound; - if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for monster", .{}); - const resource_map = resource_maps.items[0]; - - _ = try self.appendNode(TaskTreeNode{ - .parent = parent, - .character_task = .{ - .fight = .{ - .at = resource_map.position, - .target = .{ .id = item_id, .quantity = quantity } - } - } - }); + // The target item can not be currently produced. + if (subtree_id == null) { + var node = &self.nodes.items[node_id]; + try node.additional_items.add(recipe_item.id, needed_quantity); } - } else { + } + + return node_id; + } else if (item.type == .resource) { + if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) { + const resources = try self.api.getResources(.{ .drop = code }); + defer resources.deinit(); + + if (resources.items.len == 0) return error.ResourceNotFound; + if (resources.items.len > 1) std.log.warn("Multiple resources exist for target item", .{}); + const resource_code = resources.items[0].code; + + const resource_maps = try self.api.getMaps(.{ .code = resource_code }); + defer resource_maps.deinit(); + + if (resource_maps.items.len == 0) return error.MapNotFound; + if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for resource", .{}); + const resource_map = resource_maps.items[0]; + + return try self.appendNode(TaskTreeNode{ + .parent = parent, + .character_task = .{ + .gather = .{ + .at = resource_map.position, + .target = .{ .id = item_id, .quantity = quantity } + } + } + }); + } else if (eql(u8, item.subtype, "mob")) { + const monsters = try self.api.getMonsters(.{ .drop = code }); + defer monsters.deinit(); + + if (monsters.items.len == 0) return error.ResourceNotFound; + if (monsters.items.len > 1) std.log.warn("Multiple monsters exist for target item", .{}); + const monster_code = monsters.items[0].code; + + const resource_maps = try self.api.getMaps(.{ .code = monster_code }); + defer resource_maps.deinit(); + + // This monster currently doesn't exist on the map. Probably only spawns in certain situations. + if (resource_maps.items.len == 0) return null; + + if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for monster", .{}); + const resource_map = resource_maps.items[0]; + + return try self.appendNode(TaskTreeNode{ + .parent = parent, + .character_task = .{ + .fight = .{ + .at = resource_map.position, + .target = .{ .id = item_id, .quantity = quantity } + } + } + }); } } + + return null; } fn listByParent(self: *const TaskTree, parent: ?usize) !std.ArrayList(usize) { @@ -212,6 +224,31 @@ const TaskTree = struct { return found_nodes; } + fn listNextTasks(self: *const TaskTree) !std.ArrayList(*CharacterBrain.CharacterTask) { + var next_tasks = std.ArrayList(*CharacterBrain.CharacterTask).init(self.allocator); + errdefer next_tasks.deinit(); + + for (0.., self.nodes.items) |i, *node| { + var child_nodes = try self.listByParent(i); + defer child_nodes.deinit(); + + var can_be_started = true; + for (child_nodes.items) |child_node_id| { + var child_node = self.nodes.items[child_node_id]; + if (!child_node.character_task.isComplete()) { + can_be_started = false; + break; + } + } + + if (can_be_started) { + try next_tasks.append(&node.character_task); + } + } + + return next_tasks; + } + pub fn format( self: TaskTree, comptime fmt: []const u8, @@ -253,6 +290,11 @@ const TaskTree = struct { for (child_nodes.items) |child_node| { try self.formatNode(child_node, level + 1, writer); } + for (node.additional_items.slots.constSlice()) |slot| { + const item_code = self.api.getItemCode(slot.id).?; + try writer.writeBytesNTimes(" ", level+1); + try writer.print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity}); + } } }; @@ -264,13 +306,14 @@ pub fn main() !void { var api = try Server.init(allocator); defer api.deinit(); - try api.prefetch(); - const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken; defer allocator.free(token); try api.setToken(token); + std.log.info("prefetching static data", .{}); + try api.prefetch(); + var goal_manager = GoalManager.init(&api, allocator); defer goal_manager.deinit(); @@ -281,69 +324,17 @@ pub fn main() !void { try goal_manager.addCharacter(char.name); } - goal_manager.characters.items[0].task = .{ - .fight = .{ - .at = Position.init(0, 1), - .target = .{ - .id = try api.getItemId("egg"), - .quantity = 1 - } - }, - }; - - goal_manager.characters.items[1].task = .{ - .gather = .{ - .at = Position.init(-1, 0), - .target = .{ - .id = try api.getItemId("ash_wood"), - .quantity = 3 - } - } - }; - - goal_manager.characters.items[2].task = .{ - .gather = .{ - .at = Position.init(2, 0), - .target = .{ - .id = try api.getItemId("copper_ore"), - .quantity = 3 - } - } - }; - - goal_manager.characters.items[3].task = .{ - .gather = .{ - .at = Position.init(4, 2), - .target = .{ - .id = try api.getItemId("gudgeon"), - .quantity = 3 - } - } - }; - - goal_manager.characters.items[4].task = .{ - .fight = .{ - .at = Position.init(0, 1), - .target = .{ - .id = try api.getItemId("raw_chicken"), - .quantity = 1 - } - }, - }; - var task_tree = TaskTree.init(allocator, &api); defer task_tree.deinit(); - try task_tree.appendSubTree("sticky_dagger", 5, null); - // try task_tree.appendSubTree("copper_boots" , 5, null); - // try task_tree.appendSubTree("copper_helmet", 5, null); - // try task_tree.appendSubTree("copper_legs_armor", 5, null); - // try task_tree.appendSubTree("copper_armor", 5, null); - // try task_tree.appendSubTree("copper_ring", 5, null); + // _ = try task_tree.appendSubTree("wooden_staff", 5, null); + _ = try task_tree.appendSubTree("wooden_shield", 5, null); std.debug.print("{}", .{task_tree}); - if (false) { + var default_prng = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp())); + var random = default_prng.random(); + std.log.info("Starting main loop", .{}); while (true) { goal_manager.runNextAction() catch |err| switch (err) { @@ -360,15 +351,24 @@ pub fn main() !void { for (goal_manager.characters.items) |*brain| { if (brain.action_queue.items.len > 0) continue; - if (brain.isRoutineFinished()) { - if (!try brain.depositItemsToBank(&api)) { - brain.routine = null; + if (brain.isTaskFinished()) { + if (try brain.depositItemsToBank(&api)) { + continue; } - continue; + brain.task = null; } - try brain.performRoutine(&api); + 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]; + } + } + + try brain.performTask(&api); } } - } }