artificer/lib/artificer.zig

458 lines
15 KiB
Zig

// 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();
}
}
};
}