// zig fmt: off const std = @import("std"); const Api = @import("./api/root.zig"); const Allocator = std.mem.Allocator; const GatherGoal = @import("gather_goal.zig"); const CraftGoal = @import("craft_goal.zig"); const EquipGoal = @import("equip_goal.zig"); const assert = std.debug.assert; const log = std.log.scoped(.artificer); pub const GoalId = packed struct { const Generation = u5; const Index = u11; generation: Generation, index: Index, pub fn eql(self: GoalId, other: GoalId) bool { return self.index == other.index and self.generation == other.generation; } }; const max_goals = std.math.maxInt(GoalId.Index); const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms const server_down_retry_interval = 5; // minutes pub const Goal = union(enum) { gather: GatherGoal, craft: CraftGoal, equip: EquipGoal, pub fn tick(self: *Goal, ctx: *GoalContext) !void { switch (self.*) { .gather => |*gather| gather.tick(ctx), .craft => |*craft| try craft.tick(ctx), .equip => |*equip| equip.tick(ctx), } } pub fn requirements(self: Goal, ctx: *GoalContext) Requirements { return switch (self) { .gather => |gather| gather.requirements(ctx), .craft => |craft| craft.requirements(ctx), .equip => |equip| equip.requirements(ctx), }; } pub fn onActionCompleted(self: *Goal, ctx: *GoalContext, result: ActionResult) void { switch (self.*) { .gather => |*gather| gather.onActionCompleted(ctx, result), .craft => |*craft| craft.onActionCompleted(ctx, result), .equip => |*equip| equip.onActionCompleted(ctx, result), } } }; const GoalSlot = struct { generation: GoalId.Generation = 0, parent_goal: ?GoalId = null, goal: ?Goal = null, }; pub const Action = union(enum) { move: Api.Position, gather, craft: struct { item: Api.Store.Id, quantity: u64 }, unequip: struct { slot: Api.Equipment.SlotId, quantity: u64 }, equip: struct { slot: Api.Equipment.SlotId, item: Api.Store.Id, quantity: u64 }, }; pub const ActionResult = union(enum) { move: Api.MoveResult, gather: Api.GatherResult, craft: Api.CraftResult, equip: Api.EquipResult, unequip: Api.UnequipResult, }; const ActionSlot = struct { goal: GoalId, action: Action, }; pub const Requirements = struct { pub const Items = Api.SimpleItem.BoundedArray(Api.Craft.max_items); items: Items = .{} }; const QueuedActions = std.ArrayListUnmanaged(ActionSlot); pub const GoalContext = struct { goal_id: GoalId, store: *Api.Store, character: *Api.Character, queued_actions: *QueuedActions, completed: bool = false, pub fn queueAction(self: *GoalContext, action: Action) void { self.queued_actions.appendAssumeCapacity(ActionSlot{ .goal = self.goal_id, .action = action }); } pub fn findBestResourceWithItem(self: *GoalContext, item: Api.Store.Id) ?Api.Store.Id { var best_resource: ?Api.Store.Id = null; var best_rate: u64 = 0; for (0.., self.store.resources.objects.items) |resource_id, optional_resource| { if (optional_resource != .object) { continue; } const resource = optional_resource.object; const skill = resource.skill.toCharacterSkill(); const character_skill_level = self.character.skills.get(skill).level; if (character_skill_level < resource.level) { continue; } for (resource.drops.slice()) |_drop| { const drop: Api.Resource.Drop = _drop; if (drop.item != item) { continue; } // The lower the `drop.rate` the better if (best_resource == null or best_rate > drop.rate) { best_resource = resource_id; best_rate = drop.rate; break; } } } return best_resource; } pub fn findNearestMapWithResource(self: *GoalContext, resource: Api.Store.Id) ?Api.Position { const resource_code = self.store.resources.get(resource).?.code.slice(); var nearest_position: ?Api.Position = null; for (self.store.maps.items) |map| { const content = map.content orelse continue; if (content.type != .resource) { continue; } if (!std.mem.eql(u8, resource_code, content.code.slice())) { continue; } if (nearest_position == null or map.position.distance(self.character.position) < map.position.distance(nearest_position.?)) { nearest_position = map.position; } } return nearest_position; } pub fn findNearestWorkstation(self: *GoalContext, skill: Api.Craft.Skill) ?Api.Position { const skill_name = skill.toString(); var nearest_position: ?Api.Position = null; for (self.store.maps.items) |map| { const content = map.content orelse continue; if (content.type != .workshop) { continue; } if (!std.mem.eql(u8, skill_name, content.code.slice())) { continue; } if (nearest_position == null or map.position.distance(self.character.position) < map.position.distance(nearest_position.?)) { nearest_position = map.position; } } return nearest_position; } }; pub fn ArtificerType(Clock: type, Server: type) type { return struct { const Self = @This(); clock: *Clock, server: *Server, store: *Api.Store, character: Api.Store.Id, goal_slots: []GoalSlot, queued_actions: QueuedActions, pub fn init(allocator: Allocator, store: *Api.Store, clock: *Clock, server: *Server, character: Api.Store.Id) !Self { const max_queued_actions = 16; const goal_slots = try allocator.alloc(GoalSlot, max_goals); errdefer allocator.free(goal_slots); @memset(goal_slots, .{}); var queued_actions = try QueuedActions.initCapacity(allocator, max_queued_actions); errdefer queued_actions.deinit(allocator); return Self{ .clock = clock, .server = server, .store = store, .goal_slots = goal_slots, .character = character, .queued_actions = queued_actions }; } pub fn deinit(self: *Self, allocator: Allocator) void { allocator.free(self.goal_slots); self.queued_actions.deinit(allocator); } pub fn appendGoal(self: *Self, goal: Goal) !GoalId { for (0.., self.goal_slots) |index, *goal_slot| { if (goal_slot.goal != null) { continue; } if (goal_slot.generation == std.math.maxInt(GoalId.Generation)) { continue; } goal_slot.goal = goal; return GoalId{ .index = @intCast(index), .generation = goal_slot.generation }; } return error.OutOfMemory; } pub fn removeGoal(self: *Self, id: GoalId) void { if (self.getGoal(id)) |goal_slot| { goal_slot.* = .{ .generation = goal_slot.generation + 1 }; } } pub fn getGoal(self: *Self, id: GoalId) ?*GoalSlot { const slot = &self.goal_slots[id.index]; if (slot.generation != id.generation) { return null; } if (slot.goal == null) { return null; } return slot; } pub fn timeUntilCooldownExpires(self: *Self) u64 { const store = self.server.store; const character = store.characters.get(self.character).?; if (character.cooldown_expiration) |cooldown_expiration| { const cooldown_expiration_ns: i64 = @intFromFloat(cooldown_expiration * std.time.ns_per_s); const now = self.clock.nanoTimestamp(); if (cooldown_expiration_ns > now) { return @intCast(@as(i128, @intCast(cooldown_expiration_ns)) - now); } } return 0; } fn getGoalCount(self: *Self) u32 { var count: u32 = 0; for (self.goal_slots) |goal_slot| { if (goal_slot.goal == null) { continue; } count += 1; } return count; } fn hasSubGoals(self: *Self, goal_id: GoalId) bool { for (self.goal_slots) |goal_slot| { if (goal_slot.goal != null and goal_slot.parent_goal != null and goal_slot.parent_goal.?.eql(goal_id)) { return true; } } return false; } fn createGoalContext(self: *Self, goal_id: GoalId) GoalContext { return GoalContext{ .goal_id = goal_id, .queued_actions = &self.queued_actions, .character = self.store.characters.get(self.character).?, .store = self.store, .completed = false, }; } pub fn tick(self: *Self) !void { const store = self.server.store; const character = store.characters.get(self.character).?; if (self.queued_actions.items.len > 0) { const expires_in = self.timeUntilCooldownExpires(); if (expires_in > 0) { return; } const action_slot = self.queued_actions.orderedRemove(0); const character_name = character.name.slice(); log.debug("(action) {}", .{ action_slot.action }); const action_result = switch (action_slot.action) { .move => |position| ActionResult{ .move = try self.server.move(character_name, position) }, .gather => ActionResult{ .gather = try self.server.gather(character_name) }, .craft => |craft| ActionResult{ .craft = try self.server.craft(character_name, store.items.getName(craft.item).?, craft.quantity) }, .equip => |equip| ActionResult{ .equip = try self.server.equip(character_name, equip.slot, store.items.getName(equip.item).?, equip.quantity) }, .unequip => |unequip| ActionResult{ .unequip = try self.server.unequip(character_name, unequip.slot, unequip.quantity) } }; if (self.getGoal(action_slot.goal)) |goal_slot| { const goal = &goal_slot.goal.?; var goal_context = self.createGoalContext(action_slot.goal); goal.onActionCompleted(&goal_context, action_result); if (goal_context.completed) { self.removeGoal(action_slot.goal); } } } else { for (0.., self.goal_slots) |index, *goal_slot| { if (goal_slot.goal == null) { continue; } const goal = &(goal_slot.*.goal orelse continue); const goal_id = GoalId{ .index = @intCast(index), .generation = goal_slot.generation }; if (self.hasSubGoals(goal_id)) { continue; } var goal_context = self.createGoalContext(goal_id); const reqs = goal.requirements(&goal_context); for (reqs.items.slice()) |req_item| { const inventory_quantity = character.inventory.getQuantity(req_item.id); if (inventory_quantity < req_item.quantity) { const missing_quantity = req_item.quantity - inventory_quantity; const item = store.items.get(req_item.id).?; if (goal_context.findBestResourceWithItem(req_item.id) != null) { const subgoal_id = try self.appendGoal(.{ .gather = .{ .item = req_item.id, .quantity = missing_quantity } }); const subgoal = self.getGoal(subgoal_id).?; subgoal.parent_goal = goal_id; } else if (item.craft != null) { const subgoal_id = try self.appendGoal(.{ .craft = .{ .item = req_item.id, .quantity = missing_quantity } }); const subgoal = self.getGoal(subgoal_id).?; subgoal.parent_goal = goal_id; } else { @panic("Not all requirements were handled"); } } } if (self.hasSubGoals(goal_id)) { continue; } try goal.tick(&goal_context); if (goal_context.completed) { self.removeGoal(goal_id); } } } } pub fn runUntilGoalsComplete(self: *Self) !void { while (self.getGoalCount() > 0) { const expires_in = self.timeUntilCooldownExpires(); if (expires_in > 0) { log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s }); self.clock.sleep(expires_in); } try self.tick(); } } pub fn runForever(self: *Self) !void { while (true) { const expires_in = self.timeUntilCooldownExpires(); if (expires_in > 0) { log.debug("Sleeping for {d:.3}s", .{ @as(f64, @floatFromInt(expires_in)) / std.time.ns_per_s }); self.clock.sleep(expires_in); log.debug("Finished sleeping", .{}); } try self.tick(); } } }; }