artificer/src/main.zig

375 lines
12 KiB
Zig

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");
// 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,
};
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) !void {
if (quantity == 0) return;
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).?;
try self.appendSubTree(recipe_item_code, recipe_item.quantity * craft_count, node_id);
}
} else {
if (item.type == .resource) {
if (eql(u8, item.subtype, "mining")) {
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];
_ = 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();
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];
_ = try self.appendNode(TaskTreeNode{
.parent = parent,
.character_task = .{
.fight = .{
.at = resource_map.position,
.target = .{ .id = item_id, .quantity = quantity }
}
}
});
}
} else {
}
}
}
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;
}
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);
}
}
};
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();
try api.prefetch();
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
try api.setToken(token);
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);
}
goal_manager.characters.items[0].task = .{
.fight = .{
.at = Position.init(0, 1),
.target = .{
.id = try api.getItemId("egg"),
.quantity = 1
}
},
};
goal_manager.characters.items[1].task = .{
.gather = .{
.at = Position.init(-1, 0),
.target = .{
.id = try api.getItemId("ash_wood"),
.quantity = 3
}
}
};
goal_manager.characters.items[2].task = .{
.gather = .{
.at = Position.init(2, 0),
.target = .{
.id = try api.getItemId("copper_ore"),
.quantity = 3
}
}
};
goal_manager.characters.items[3].task = .{
.gather = .{
.at = Position.init(4, 2),
.target = .{
.id = try api.getItemId("gudgeon"),
.quantity = 3
}
}
};
goal_manager.characters.items[4].task = .{
.fight = .{
.at = Position.init(0, 1),
.target = .{
.id = try api.getItemId("raw_chicken"),
.quantity = 1
}
},
};
var task_tree = TaskTree.init(allocator, &api);
defer task_tree.deinit();
try task_tree.appendSubTree("sticky_dagger", 5, null);
// try task_tree.appendSubTree("copper_boots" , 5, null);
// try task_tree.appendSubTree("copper_helmet", 5, null);
// try task_tree.appendSubTree("copper_legs_armor", 5, null);
// try task_tree.appendSubTree("copper_armor", 5, null);
// try task_tree.appendSubTree("copper_ring", 5, null);
std.debug.print("{}", .{task_tree});
if (false) {
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.isRoutineFinished()) {
if (!try brain.depositItemsToBank(&api)) {
brain.routine = null;
}
continue;
}
try brain.performRoutine(&api);
}
}
}
}