const std = @import("std"); const Allocator = std.mem.Allocator; 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 }; fn todo() void { unreachable; } fn currentTime() f64 { const timestamp: f64 = @floatFromInt(std.time.milliTimestamp()); return timestamp / std.time.ms_per_s; } const GoalManager = struct { api: *Server, allocator: Allocator, characters: std.ArrayList(CharacterBrain), expiration_margin: f64 = 0.1, // 100ms fn init(api: *Server, allocator: Allocator) GoalManager { return GoalManager{ .api = api, .allocator = allocator, .characters = std.ArrayList(CharacterBrain).init(allocator) }; } fn addCharacter(self: *GoalManager, name: []const u8) !void { const character = try CharacterBrain.init(self.allocator, name); try self.characters.append(character); } fn deinit(self: GoalManager) void { for (self.characters.items) |brain| { brain.deinit(); } self.characters.deinit(); } fn runNextAction(self: *GoalManager) !void { var earliest_cooldown: f64 = 0; var earliest_character: ?*CharacterBrain = null; for (self.characters.items) |*brain| { if (brain.action_queue.items.len == 0) continue; const character = self.api.findCharacter(brain.name).?; if (earliest_character == null or earliest_cooldown > character.cooldown_expiration) { earliest_character = brain; earliest_cooldown = character.cooldown_expiration; } } if (earliest_character == null) return; const now = currentTime(); if (earliest_cooldown > now) { const duration_s = earliest_cooldown - now + self.expiration_margin; const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s)); std.log.debug("waiting for {d:.3}s", .{duration_s}); std.time.sleep(std.time.ns_per_ms * duration_ms); } try earliest_character.?.performNextAction(self.api); } }; fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 { const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len < 2) { return null; } const filename = args[1]; const cwd = std.fs.cwd(); var token_buffer: [256]u8 = undefined; const token = try cwd.readFile(filename, &token_buffer); return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t ")); } const TaskTree = struct { const TaskTreeNode = struct { parent: ?usize, character_task: CharacterBrain.CharacterTask, }; const Nodes = std.ArrayList(TaskTreeNode); allocator: Allocator, nodes: Nodes, api: *Server, pub fn init(allocator: Allocator, api: *Server) TaskTree { return TaskTree{ .allocator = allocator, .nodes = Nodes.init(allocator), .api = api }; } pub fn deinit(self: TaskTree) void { self.nodes.deinit(); } fn appendNode(self: *TaskTree, node: TaskTreeNode) !usize { try self.nodes.append(node); return self.nodes.items.len-1; } fn appendSubTree(self: *TaskTree, code: []const u8, quantity: u64, parent: ?usize) !void { if (quantity == 0) return; const item = (try self.api.getItem(code)).?; const item_id = try self.api.getItemId(code); const eql = std.mem.eql; if (item.craft) |recipe| { const craft_count = std.math.divCeil(u64, quantity, recipe.quantity) catch unreachable; const skill_str = Server.SkillUtils.toString(recipe.skill); const workshop_maps = try self.api.getMaps(.{ .code = skill_str, .type = .workshop }); defer workshop_maps.deinit(); if (workshop_maps.items.len == 0) return error.WorkshopNotFound; if (workshop_maps.items.len > 1) std.log.warn("Multiple workshop locations exist", .{}); const node_id = try self.appendNode(TaskTreeNode{ .parent = parent, .character_task = .{ .craft = .{ .at = workshop_maps.items[0].position, .target = .{ .id = item_id, .quantity = craft_count } } } }); 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(); 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 } } } }); } } else { } } } fn listByParent(self: *const TaskTree, parent: ?usize) !std.ArrayList(usize) { var found_nodes = std.ArrayList(usize).init(self.allocator); for (0.., self.nodes.items) |i, node| { if (node.parent == parent) { try found_nodes.append(i); } } return found_nodes; } pub fn format( self: TaskTree, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, ) !void { _ = fmt; _ = options; var root_nodes = try self.listByParent(null); defer root_nodes.deinit(); for (root_nodes.items) |root_node| { try self.formatNode(root_node, 0, writer); } } fn formatNode(self: TaskTree, node_id: usize, level: u32, writer: anytype) !void { const node = self.nodes.items[node_id]; try writer.writeBytesNTimes(" ", level); switch (node.character_task) { .fight => |args| { const item = self.api.getItemCode(args.target.id).?; try writer.print("Task{{ .fight = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at}); }, .gather => |args| { const item = self.api.getItemCode(args.target.id).?; try writer.print("Task{{ .gather = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at}); }, .craft => |args| { const item = self.api.getItemCode(args.target.id).?; try writer.print("Task{{ .craft = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at}); }, } var child_nodes = try self.listByParent(node_id); defer child_nodes.deinit(); for (child_nodes.items) |child_node| { try self.formatNode(child_node, level + 1, writer); } } }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); 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); var goal_manager = GoalManager.init(&api, allocator); defer goal_manager.deinit(); const chars = try api.listMyCharacters(); defer chars.deinit(); for (chars.items) |char| { try goal_manager.addCharacter(char.name); } 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); std.debug.print("{}", .{task_tree}); if (false) { std.log.info("Starting main loop", .{}); while (true) { goal_manager.runNextAction() catch |err| switch (err) { Server.APIError.ServerUnavailable => { // If the server is down, wait for a moment and try again. std.time.sleep(std.time.ns_per_min * 5); continue; }, // TODO: Log all other error to a file or something. So it could be review later on. else => return err }; 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; } continue; } try brain.performRoutine(&api); } } } }