458 lines
15 KiB
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();
|
|
}
|
|
}
|
|
};
|
|
}
|