diff --git a/src/api/character.zig b/api/character.zig similarity index 100% rename from src/api/character.zig rename to api/character.zig diff --git a/src/api/combat_stats.zig b/api/combat_stats.zig similarity index 100% rename from src/api/combat_stats.zig rename to api/combat_stats.zig diff --git a/src/date_time/parse.zig b/api/date_time/parse.zig similarity index 100% rename from src/date_time/parse.zig rename to api/date_time/parse.zig diff --git a/src/date_time/timegm.c b/api/date_time/timegm.c similarity index 100% rename from src/date_time/timegm.c rename to api/date_time/timegm.c diff --git a/src/date_time/timegm.h b/api/date_time/timegm.h similarity index 100% rename from src/date_time/timegm.h rename to api/date_time/timegm.h diff --git a/src/api/enum_string_utils.zig b/api/enum_string_utils.zig similarity index 100% rename from src/api/enum_string_utils.zig rename to api/enum_string_utils.zig diff --git a/src/api/equipment.zig b/api/equipment.zig similarity index 100% rename from src/api/equipment.zig rename to api/equipment.zig diff --git a/src/api/json_utils.zig b/api/json_utils.zig similarity index 100% rename from src/api/json_utils.zig rename to api/json_utils.zig diff --git a/api/position.zig b/api/position.zig new file mode 100644 index 0000000..c2e2f23 --- /dev/null +++ b/api/position.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const Position = @This(); + +x: i64, +y: i64, + +pub fn init(x: i64, y: i64) Position { + return Position{ + .x = x, + .y = y + }; +} + +pub fn eql(self: Position, other: Position) bool { + return self.x == other.x and self.y == other.y; +} + +pub fn format( + self: Position, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, +) !void { + _ = fmt; + _ = options; + + try writer.print("{{ {}, {} }}", .{self.x, self.y}); +} diff --git a/api/root.zig b/api/root.zig new file mode 100644 index 0000000..06f3a7f --- /dev/null +++ b/api/root.zig @@ -0,0 +1,8 @@ + +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 ItemId = Server.ItemId; +pub const ItemIdQuantity = Server.ItemIdQuantity; diff --git a/src/api/server.zig b/api/server.zig similarity index 99% rename from src/api/server.zig rename to api/server.zig index ba6c0c3..fe2a717 100644 --- a/src/api/server.zig +++ b/api/server.zig @@ -4,7 +4,7 @@ const json = std.json; const Allocator = std.mem.Allocator; // TODO: Maybe it would be good to move date time parsing to separate module -pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime; +pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime; const json_utils = @import("json_utils.zig"); pub const Character = @import("character.zig"); @@ -303,6 +303,13 @@ pub const ItemIdQuantity = struct { id: ItemId, quantity: u64, + pub fn init(id: ItemId, quantity: u64) ItemIdQuantity { + return ItemIdQuantity{ + .id = id, + .quantity = quantity + }; + } + pub fn parse(api: *Server, obj: json.ObjectMap) !ItemIdQuantity { const code = try json_utils.getStringRequired(obj, "code"); const quantity = try json_utils.getIntegerRequired(obj, "quantity"); @@ -1851,6 +1858,11 @@ pub fn getItem(self: *Server, code: []const u8) APIError!?Item { } } +pub fn getItemById(self: *Server, id: ItemId) APIError!?Item { + const code = self.getItemCode(id) orelse return null; + return self.getItem(code); +} + pub const ItemOptions = struct { craft_material: ?[]const u8 = null, craft_skill: ?Skill = null, diff --git a/src/api/skill_stats.zig b/api/skill_stats.zig similarity index 100% rename from src/api/skill_stats.zig rename to api/skill_stats.zig diff --git a/src/api/slot.zig b/api/slot.zig similarity index 100% rename from src/api/slot.zig rename to api/slot.zig diff --git a/src/api/slot_array.zig b/api/slot_array.zig similarity index 100% rename from src/api/slot_array.zig rename to api/slot_array.zig diff --git a/build.zig b/build.zig index 015b346..ceaa814 100644 --- a/build.zig +++ b/build.zig @@ -1,41 +1,91 @@ const std = @import("std"); +const ResolvedTarget = std.Build.ResolvedTarget; +const OptimizeMode = std.Build.OptimizeMode; +const Module = std.Build.Module; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const exe = b.addExecutable(.{ - .name = "artificer", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - exe.linkLibC(); - exe.addIncludePath(b.path("src/date_time")); - exe.addCSourceFile(.{ .file = b.path("src/date_time/timegm.c") }); - - b.installArtifact(exe); - - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - - if (b.args) |args| { - run_cmd.addArgs(args); + var api: *Module = undefined; + { + api = b.createModule(.{ + .root_source_file = b.path("api/root.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + api.addIncludePath(b.path("api/date_time")); + api.addCSourceFile(.{ .file = b.path("api/date_time/timegm.c") }); } - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); + var lib: *Module = undefined; + { + lib = b.createModule(.{ + .root_source_file = b.path("lib/root.zig"), + .target = target, + .optimize = optimize, + }); + lib.addImport("artifacts-api", api); - const exe_unit_tests = b.addTest(.{ - .root_source_file = b.path("src/artifacts.zig"), - .target = target, - .optimize = optimize, - }); - exe_unit_tests.linkLibC(); - exe_unit_tests.addIncludePath(b.path("src")); - exe_unit_tests.addCSourceFile(.{ .file = b.path("src/timegm.c") }); + const unit_tests = b.addTest(.{ + .root_source_file = b.path("lib/root.zig"), + .target = target, + .optimize = optimize, + }); - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_exe_unit_tests.step); + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test-lib", "Run lib unit tests"); + test_step.dependOn(&run_unit_tests.step); + } + + { + const cli = b.addExecutable(.{ + .name = "artificer", + .root_source_file = b.path("cli/main.zig"), + .target = target, + .optimize = optimize, + }); + cli.root_module.addImport("artificer", lib); + b.installArtifact(cli); + + const run_cmd = b.addRunArtifact(cli); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run-cli", "Run the CLI"); + run_step.dependOn(&run_cmd.step); + } + + { + const raylib_dep = b.dependency("raylib-zig", .{ + .target = target, + .optimize = optimize, + }); + + const gui = b.addExecutable(.{ + .name = "artificer-gui", + .root_source_file = b.path("gui/main.zig"), + .target = target, + .optimize = optimize, + }); + gui.root_module.addImport("artificer", lib); + gui.linkLibrary(raylib_dep.artifact("raylib")); + gui.root_module.addImport("raylib", raylib_dep.module("raylib")); + + b.installArtifact(gui); + + const run_cmd = b.addRunArtifact(gui); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run-gui", "Run the GUI"); + run_step.dependOn(&run_cmd.step); + } } diff --git a/build.zig.zon b/build.zig.zon index dadd4eb..4338e9e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,6 +2,11 @@ .name = "artificer", .version = "0.1.0", .minimum_zig_version = "0.12.0", - .dependencies = .{ }, + .dependencies = .{ + .@"raylib-zig" = .{ + .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", + .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" + }, + }, .paths = .{ "" }, } diff --git a/cli/main.zig b/cli/main.zig new file mode 100644 index 0000000..160a077 --- /dev/null +++ b/cli/main.zig @@ -0,0 +1,42 @@ +const std = @import("std"); +const Artificer = @import("artificer"); +const Allocator = std.mem.Allocator; + +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 ")); +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken; + defer allocator.free(token); + + var artificer = try Artificer.init(allocator, token); + defer artificer.deinit(); + + std.log.info("Starting main loop", .{}); + while (true) { + const waitUntil = artificer.nextStepAt(); + const duration = waitUntil - std.time.timestamp(); + if (duration > 0) { + std.time.sleep(@intCast(duration)); + } + + try artificer.step(); + } +} diff --git a/gui/main.zig b/gui/main.zig new file mode 100644 index 0000000..4ae120f --- /dev/null +++ b/gui/main.zig @@ -0,0 +1,18 @@ +const rl = @import("raylib"); + +pub fn main() anyerror!void { + rl.initWindow(800, 450, "Artificer"); + defer rl.closeWindow(); + + rl.setTargetFPS(60); + + while (!rl.windowShouldClose()) { + + rl.beginDrawing(); + defer rl.endDrawing(); + + rl.clearBackground(rl.Color.white); + + rl.drawText("Congrats! You created your first window!", 190, 200, 20, rl.Color.light_gray); + } +} diff --git a/src/character_brain.zig b/lib/brain.zig similarity index 95% rename from src/character_brain.zig rename to lib/brain.zig index 62dbe1c..af75fd2 100644 --- a/src/character_brain.zig +++ b/lib/brain.zig @@ -1,9 +1,12 @@ const std = @import("std"); -const Server = @import("./api/server.zig"); +const ArtifactsAPI = @import("artifacts-api"); +const Server = ArtifactsAPI.Server; const Allocator = std.mem.Allocator; const Position = Server.Position; const assert = std.debug.assert; +const CharacterTask = @import("./task.zig").Task; + const CharacterBrain = @This(); const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically @@ -98,38 +101,6 @@ comptime { assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len); } -pub const CharacterTask = union(enum) { - fight: struct { - at: Position, - target: Server.ItemIdQuantity, - progress: u64 = 0, - }, - gather: struct { - at: Position, - target: Server.ItemIdQuantity, - progress: u64 = 0, - }, - craft: struct { - at: Position, - target: Server.ItemIdQuantity, - progress: u64 = 0, - }, - - pub fn isComplete(self: CharacterTask) bool { - return switch (self) { - .fight => |args| { - return args.progress >= args.target.quantity; - }, - .gather => |args| { - return args.progress >= args.target.quantity; - }, - .craft => |args| { - return args.progress >= args.target.quantity; - } - }; - } -}; - name: []const u8, action_queue: std.ArrayList(QueuedAction), task: ?*CharacterTask = null, @@ -397,7 +368,7 @@ fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void { const fight_result: Server.FightResult = r; const drops = fight_result.fight.drops; - args.progress += drops.getQuantity(args.target.id); + args.progress += drops.getQuantity(args.until.item.id); } }, .gather => |*args| { @@ -405,7 +376,7 @@ fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void { const gather_resutl: Server.GatherResult = r; const items = gather_resutl.details.items; - args.progress += items.getQuantity(args.target.id); + args.progress += items.getQuantity(args.until.item.id); } }, .craft => |*args| { diff --git a/lib/root.zig b/lib/root.zig new file mode 100644 index 0000000..2500b95 --- /dev/null +++ b/lib/root.zig @@ -0,0 +1,134 @@ +const std = @import("std"); +const Api = @import("artifacts-api"); +const Allocator = std.mem.Allocator; + +const CharacterBrain = @import("./brain.zig"); + +const Artificer = @This(); + +const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms +const server_down_retry_interval = 5; // minutes + +server: Api.Server, +characters: std.ArrayList(CharacterBrain), + +paused_until: ?i64 = null, + +pub fn init(allocator: Allocator, token: []const u8) !Artificer { + var server = try Api.Server.init(allocator); + errdefer server.deinit(); + + try server.setToken(token); + + var characters = std.ArrayList(CharacterBrain).init(allocator); + defer characters.deinit(); + // TODO: Add character deinit + + // const chars = try api.listMyCharacters(); + // defer chars.deinit(); + + // for (chars.items) |char| { + // try scheduler.addCharacter(char.name); + // } + + return Artificer{ + .server = server, + .characters = characters + }; +} + +pub fn deinit(self: Artificer) void { + for (self.characters.items) |brain| { + brain.deinit(); + } + self.characters.deinit(); +} + +pub fn step(self: *Artificer) !void { + if (self.paused_until) |paused_until| { + if (std.time.timestamp() < paused_until) { + return; + } + self.paused_until = null; + } + + runNextActions(self.characters.items, &self.server) catch |err| switch (err) { + Api.Error.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; + }, + else => return err + }; + + 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]; + // } + } + + try brain.performTask(&self.server); + } +} + +pub fn nextStepAt(self: *Artificer) i64 { + if (self.paused_until) |paused_until| { + return paused_until; + } + + return earliestCooldown(self.characters.items, &self.server) orelse 0; +} + +fn earliestCooldown(characters: []CharacterBrain, api: *Api.Server) ?i64 { + var earliest_cooldown: ?i64 = null; + for (characters) |*brain| { + if (brain.action_queue.items.len == 0) continue; + + const character = api.findCharacter(brain.name).?; + const cooldown: i64 = @intFromFloat(character.cooldown_expiration * std.time.ns_per_s); + + if (earliest_cooldown == null or earliest_cooldown.? > cooldown) { + earliest_cooldown = cooldown; + } + } + + return earliest_cooldown; +} + +fn runNextActions(characters: []CharacterBrain, api: *Api.Server) !void { + const maybe_earliest_cooldown = earliestCooldown(characters, api); + if (maybe_earliest_cooldown == null) return; + + const earliest_cooldown = maybe_earliest_cooldown.?; + if (earliest_cooldown < std.time.timestamp()) return; + + for (characters) |*brain| { + if (brain.action_queue.items.len == 0) continue; + + const character = api.findCharacter(brain.name).?; + const cooldown: u64 = @intFromFloat(character.cooldown_expiration * std.time.ns_per_s); + + if (earliest_cooldown > cooldown) { + try brain.performNextAction(api); + } + } +} + +fn currentTime() f64 { + const timestamp: f64 = @floatFromInt(std.time.milliTimestamp()); + return timestamp / std.time.ms_per_s; +} diff --git a/lib/task.zig b/lib/task.zig new file mode 100644 index 0000000..33e648c --- /dev/null +++ b/lib/task.zig @@ -0,0 +1,39 @@ +const Api = @import("artifacts-api"); +const Position = Api.Position; + +pub const UntilCondition = union(enum) { + xp: u64, + item: Api.ItemIdQuantity, +}; + +pub const Task = union(enum) { + fight: struct { + at: Position, + until: UntilCondition, + progress: u64 = 0, + }, + gather: struct { + at: Position, + until: UntilCondition, + progress: u64 = 0, + }, + craft: struct { + at: Position, + target: Api.ItemIdQuantity, + progress: u64 = 0, + }, + + 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; + } + }; + } +}; diff --git a/lib/task_graph.zig b/lib/task_graph.zig new file mode 100644 index 0000000..2641351 --- /dev/null +++ b/lib/task_graph.zig @@ -0,0 +1,234 @@ +const std = @import("std"); +const Api = @import("artifacts-api"); +const Allocator = std.mem.Allocator; + +const TaskGraph = @This(); +const CharacterTask = @import("./task.zig").Task; + +const TaskNodeId = u16; +const TaskNode = struct { + const Dependencies = std.BoundedArray(TaskNodeId, 8); + const MissingItems = Api.BoundedSlotsArray(8); + + task: CharacterTask, + dependencies: Dependencies = Dependencies.init(0) catch unreachable, + missing_items: MissingItems = MissingItems.init(), + + pub fn format( + self: TaskNode, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = options; + _ = fmt; + + switch (self.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); + // } + // 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}); + // } + } +}; + +const Nodes = std.ArrayList(TaskNode); + +nodes: Nodes, + +pub fn init(allocator: Allocator) TaskGraph { + return TaskGraph{ .nodes = Nodes.init(allocator) }; +} + +pub fn deinit(self: TaskGraph) void { + self.nodes.deinit(); +} + +fn get(self: *TaskGraph, id: TaskNodeId) *TaskNode { + return &self.nodes.items[id]; +} + +fn addTask(self: *TaskGraph, node: TaskNode) !TaskNodeId { + try self.nodes.append(node); + return @intCast(self.nodes.items.len-1); +} + +fn addFightTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId { + const item_code = api.getItemCode(item_id) orelse return error.ItemNotFound; + const monsters = try api.getMonsters(.{ .drop = item_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 error.MapNotFound; + + 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.addTask(TaskNode{ + .task = .{ + .fight = .{ + .at = resource_map.position, + .until = .{ .item = Api.ItemIdQuantity.init(item_id, quantity) } + } + } + }); +} + +fn addGatherTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId { + const item_code = api.getItemCode(item_id) orelse return error.ItemNotFound; + const resources = try api.getResources(.{ .drop = item_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.addTask(TaskNode{ + .task = .{ + .gather = .{ + .at = resource_map.position, + .until = .{ .item = Api.ItemIdQuantity.init(item_id, quantity) } + } + } + }); +} + +fn addCraftTaskShallow(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId { + const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound; + const recipe = item.craft orelse return error.RecipeNotFound; + + const skill_str = Api.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", .{}); + + return try self.addTask(TaskNode{ + .task = .{ + .craft = .{ + .at = workshop_maps.items[0].position, + .target = Api.ItemIdQuantity.init(item_id, quantity) + } + } + }); +} + +fn addCraftTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId { + const node_id = try self.addCraftTaskShallow(api, item_id, quantity); + var node = self.get(node_id); + + const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound; + const recipe = item.craft orelse return error.RecipeNotFound; + + const craft_count = recipe.quantity; + + for (recipe.items.slots.constSlice()) |material| { + const needed_quantity = material.quantity * craft_count; + + if (try self.addAutoTask(api, material.id, needed_quantity)) |dependency_id| { + try node.dependencies.append(dependency_id); + } else { + try node.missing_items.add(material.id, needed_quantity); + } + } + + return node_id; +} + +// TODO: Remove `anyerror` from function declaration +fn addAutoTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) anyerror!?TaskNodeId { + const item = (try self.api.getItemById(item_id)) orelse return error.ItemNotFound; + + if (item.craft != null) { + return try self.addCraftTask(api, item_id, quantity); + } else if (item.type == .resource) { + const eql = std.mem.eql; + if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) { + return try self.addGatherTask(api, item_id, quantity); + } else if (eql(u8, item.subtype, "mob")) { + return try self.addFightTask(api, item_id, quantity); + } + } + + return null; +} + +fn printTask(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId) void { + self.printTaskLevel(api, node_id, 0); +} + +fn writeIdentation(level: u32) void { + const mutex = std.debug.getStderrMutex(); + mutex.lock(); + defer mutex.unlock(); + + const stderr = std.io.getStdErr().writer(); + stderr.writeBytesNTimes(" ", level) catch return; +} + +fn printTaskLevel(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId, level: u32) void { + const node = self.get(node_id); + const print = std.debug.print; + + writeIdentation(level); + switch (node.task) { + .fight => |args| { + const target_item = args.until.item; + const item = api.getItemCode(target_item.id).?; + print("Task{{ .fight = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at}); + }, + .gather => |args| { + const target_item = args.until.item; + const item = api.getItemCode(target_item.id).?; + print("Task{{ .gather = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at}); + }, + .craft => |args| { + const item = api.getItemCode(args.target.id).?; + print("Task{{ .craft = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at}); + }, + } + + for (node.dependencies.constSlice()) |dependency| { + self.printTaskLevel(dependency, level + 1); + } + for (node.missing_items.slots.constSlice()) |slot| { + const item_code = api.getItemCode(slot.id).?; + writeIdentation(level+1); + print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity}); + } +} diff --git a/src/api/position.zig b/src/api/position.zig deleted file mode 100644 index f8cf052..0000000 --- a/src/api/position.zig +++ /dev/null @@ -1,15 +0,0 @@ -const Position = @This(); - -x: i64, -y: i64, - -pub fn init(x: i64, y: i64) Position { - return Position{ - .x = x, - .y = y - }; -} - -pub fn eql(self: Position, other: Position) bool { - return self.x == other.x and self.y == other.y; -} diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index f826514..0000000 --- a/src/main.zig +++ /dev/null @@ -1,374 +0,0 @@ -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"); -const BoundedSlotsArray = @import("./api/slot_array.zig").BoundedSlotsArray; - -// 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, - additional_items: BoundedSlotsArray(8) = BoundedSlotsArray(8).init(), - }; - - 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) !?usize { - if (quantity == 0) return null; - - 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).?; - const needed_quantity = recipe_item.quantity * craft_count; - const subtree_id = try self.appendSubTree(recipe_item_code, needed_quantity, node_id); - - // 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); - } - } - - 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) { - 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; - } - - 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, - 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); - } - 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}); - } - } -}; - -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(); - - 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(); - - const chars = try api.listMyCharacters(); - defer chars.deinit(); - - for (chars.items) |char| { - try goal_manager.addCharacter(char.name); - } - - var task_tree = TaskTree.init(allocator, &api); - defer task_tree.deinit(); - - // _ = try task_tree.appendSubTree("wooden_staff", 5, null); - _ = try task_tree.appendSubTree("wooden_shield", 5, null); - - std.debug.print("{}", .{task_tree}); - - 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) { - 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.isTaskFinished()) { - if (try brain.depositItemsToBank(&api)) { - 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]; - } - } - - try brain.performTask(&api); - } - } -}