const std = @import("std"); const assert = std.debug.assert; const ArtifactsAPI = @import("artifacts.zig"); const Allocator = std.mem.Allocator; // pub const std_options = .{ .log_level = .debug }; const Position = struct { x: i64, y: i64, fn init(x: i64, y: i64) Position { return Position{ .x = x, .y = y }; } fn eql(self: Position, other: Position) bool { return self.x == other.x and self.y == other.y; } }; const QueuedAction = union(enum) { move: Position, attack, gather, depositGold: u64, depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 }, }; const ActionQueue = std.ArrayList(QueuedAction); const ManagedCharacter = struct { character: ArtifactsAPI.Character, action_queue: ActionQueue, cooldown_expires_at: f64, }; fn currentTime() f64 { const timestamp: f64 = @floatFromInt(std.time.milliTimestamp()); return timestamp / std.time.ms_per_s; } const Manager = struct { allocator: Allocator, characters: std.ArrayList(ManagedCharacter), api: *ArtifactsAPI, fn init(allocator: Allocator, api: *ArtifactsAPI) Manager { return Manager{ .allocator = allocator, .api = api, .characters = std.ArrayList(ManagedCharacter).init(allocator), }; } fn addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void { try self.characters.append(ManagedCharacter{ .character = character, .action_queue = std.ArrayList(QueuedAction).init(self.allocator), .cooldown_expires_at = character.cooldown_expiration }); } fn poll(self: *Manager) ?*ManagedCharacter { if (self.characters.items.len == 0) return null; var earliest_expiration = self.characters.items[0].cooldown_expires_at; var now = currentTime(); for (self.characters.items) |managed_character| { earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at); } if (earliest_expiration > now) { const duration_s = earliest_expiration - now; 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); } const cooldown_margin = 0.1; // 100ms now = currentTime(); for (self.characters.items) |*managed_character| { if (now - managed_character.cooldown_expires_at >= -cooldown_margin) { return managed_character; } } return null; } fn deinit(self: Manager) void { for (self.characters.items) |managed_character| { managed_character.action_queue.deinit(); } self.characters.deinit(); } }; fn depositIfFull(managed_char: *ManagedCharacter) !bool { const character = managed_char.character; const action_queue = &managed_char.action_queue; // Deposit items and gold to bank if full if (character.getItemCount() < character.inventory_max_items) { return false; } var character_pos = Position.init(character.x, character.y); const bank_pos = Position{ .x = 4, .y = 1 }; if (!character_pos.eql(bank_pos)) { try action_queue.append(.{ .move = bank_pos }); } for (character.inventory.slots) |slot| { if (slot.quantity == 0) continue; if (slot.id) |item_id| { try action_queue.append(.{ .depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) } }); } } if (character.gold > 0) { try action_queue.append(.{ .depositGold = @intCast(character.gold) }); } return true; } fn attackChickenRoutine(managed_char: *ManagedCharacter) !void { const character = managed_char.character; const action_queue = &managed_char.action_queue; const chicken_pos = Position{ .x = 0, .y = 1 }; var character_pos = Position.init(character.x, character.y); // Deposit items and gold to bank if full if (try depositIfFull(managed_char)) { return; } // Go to chickens if (!character_pos.eql(chicken_pos)) { try action_queue.append(.{ .move = chicken_pos }); return; } // Attack chickens try action_queue.append(.{ .attack = {} }); } fn gatherResourceRoutine(managed_char: *ManagedCharacter, resource_pos: Position) !void { const character = managed_char.character; const action_queue = &managed_char.action_queue; var character_pos = Position.init(character.x, character.y); // Deposit items and gold to bank if full if (try depositIfFull(managed_char)) { return; } if (!character_pos.eql(resource_pos)) { try action_queue.append(.{ .move = resource_pos }); return; } try action_queue.append(.{ .gather = {} }); } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var api = try ArtifactsAPI.init(allocator); defer api.deinit(); const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len >= 2) { const filename = args[1]; const cwd = std.fs.cwd(); var token_buffer: [256]u8 = undefined; const token = try cwd.readFile(filename, &token_buffer); try api.setToken(std.mem.trim(u8,token,"\n\t ")); } if (api.token == null) { return error.MissingToken; } var manager = Manager.init(allocator, &api); defer manager.deinit(); const characters = try api.listMyCharacters(); defer characters.deinit(); for (characters.items) |character| { try manager.addCharacter(character); } const status = try api.getServerStatus(); defer status.deinit(); std.log.info("Server status: {s} v{s}", .{ status.status, status.version }); std.log.info("Characters online: {}", .{ status.characters_online }); std.log.info("Starting main loop", .{}); while (manager.poll()) |char| { if (char.action_queue.items.len > 0) { const action = char.action_queue.items[0]; var cooldown: ArtifactsAPI.Cooldown = undefined; switch (action) { .attack => { std.log.debug("{s} attacks", .{char.character.name}); var result = try api.actionFight(char.character.name); cooldown = result.cooldown; char.character.gold += result.fight.gold; for (result.fight.drops.slice()) |item| { char.character.inventory.addItem(item.id, @intCast(item.quantity)); } }, .move => |pos| { std.log.debug("move {s} to ({}, {})", .{char.character.name, pos.x, pos.y}); var result = try api.actionMove(char.character.name, pos.x, pos.y); defer result.deinit(); cooldown = result.cooldown; char.character.x = pos.x; char.character.y = pos.y; }, .depositGold => |quantity| { std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name}); var result = try api.actionBankDepositGold(char.character.name, quantity); defer result.deinit(); cooldown = result.cooldown; char.character.gold -= @intCast(quantity); assert(char.character.gold >= 0); }, .depositItem => |item| { std.log.debug("deposit {s}(x{}) from {s}", .{api.getItemCode(item.id).?, item.quantity, char.character.name}); const code = api.getItemCode(item.id) orelse return error.ItemNotFound; var result = try api.actionBankDepositItem(char.character.name, code, item.quantity); defer result.deinit(); cooldown = result.cooldown; char.character.inventory.removeItem(item.id, item.quantity); }, .gather => { std.log.debug("{s} gathers", .{char.character.name}); var result = try api.actionGather(char.character.name); cooldown = result.cooldown; for (result.details.items.slice()) |item| { char.character.inventory.addItem(item.id, @intCast(item.quantity)); } } } char.cooldown_expires_at = cooldown.expiration; _ = char.action_queue.orderedRemove(0); continue; } if (std.mem.eql(u8, char.character.name, "Devin")) { try gatherResourceRoutine(char, .{ .x = -1, .y = 0 }); // Ash trees } else if (std.mem.eql(u8, char.character.name, "Dawn")) { try gatherResourceRoutine(char, .{ .x = 2, .y = 0 }); // Copper ore } else if (std.mem.eql(u8, char.character.name, "Diana")) { try gatherResourceRoutine(char, .{ .x = 4, .y = 2 }); // Gudgeon fish } else { try attackChickenRoutine(char); } } }