split up into multiple modules

This commit is contained in:
Rokas Puzonas 2024-09-07 15:49:47 +03:00
parent 64c4d9ff47
commit 45e32424cb
24 changed files with 608 additions and 456 deletions

28
api/position.zig Normal file
View File

@ -0,0 +1,28 @@
const std = @import("std");
const Position = @This();
x: i64,
y: i64,
pub fn init(x: i64, y: i64) Position {
return Position{
.x = x,
.y = y
};
}
pub fn eql(self: Position, other: Position) bool {
return self.x == other.x and self.y == other.y;
}
pub fn format(
self: Position,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{{ {}, {} }}", .{self.x, self.y});
}

8
api/root.zig Normal file
View File

@ -0,0 +1,8 @@
pub const Server = @import("server.zig");
pub const Position = @import("position.zig");
pub const BoundedSlotsArray = @import("slot_array.zig").BoundedSlotsArray;
pub const Error = Server.APIError;
pub const ItemId = Server.ItemId;
pub const ItemIdQuantity = Server.ItemIdQuantity;

View File

@ -4,7 +4,7 @@ const json = std.json;
const Allocator = std.mem.Allocator;
// TODO: Maybe it would be good to move date time parsing to separate module
pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime;
const json_utils = @import("json_utils.zig");
pub const Character = @import("character.zig");
@ -303,6 +303,13 @@ pub const ItemIdQuantity = struct {
id: ItemId,
quantity: u64,
pub fn init(id: ItemId, quantity: u64) ItemIdQuantity {
return ItemIdQuantity{
.id = id,
.quantity = quantity
};
}
pub fn parse(api: *Server, obj: json.ObjectMap) !ItemIdQuantity {
const code = try json_utils.getStringRequired(obj, "code");
const quantity = try json_utils.getIntegerRequired(obj, "quantity");
@ -1851,6 +1858,11 @@ pub fn getItem(self: *Server, code: []const u8) APIError!?Item {
}
}
pub fn getItemById(self: *Server, id: ItemId) APIError!?Item {
const code = self.getItemCode(id) orelse return null;
return self.getItem(code);
}
pub const ItemOptions = struct {
craft_material: ?[]const u8 = null,
craft_skill: ?Skill = null,

110
build.zig
View File

@ -1,41 +1,91 @@
const std = @import("std");
const ResolvedTarget = std.Build.ResolvedTarget;
const OptimizeMode = std.Build.OptimizeMode;
const Module = std.Build.Module;
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "artificer",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibC();
exe.addIncludePath(b.path("src/date_time"));
exe.addCSourceFile(.{ .file = b.path("src/date_time/timegm.c") });
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
var api: *Module = undefined;
{
api = b.createModule(.{
.root_source_file = b.path("api/root.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
api.addIncludePath(b.path("api/date_time"));
api.addCSourceFile(.{ .file = b.path("api/date_time/timegm.c") });
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
var lib: *Module = undefined;
{
lib = b.createModule(.{
.root_source_file = b.path("lib/root.zig"),
.target = target,
.optimize = optimize,
});
lib.addImport("artifacts-api", api);
const exe_unit_tests = b.addTest(.{
.root_source_file = b.path("src/artifacts.zig"),
.target = target,
.optimize = optimize,
});
exe_unit_tests.linkLibC();
exe_unit_tests.addIncludePath(b.path("src"));
exe_unit_tests.addCSourceFile(.{ .file = b.path("src/timegm.c") });
const unit_tests = b.addTest(.{
.root_source_file = b.path("lib/root.zig"),
.target = target,
.optimize = optimize,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_exe_unit_tests.step);
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test-lib", "Run lib unit tests");
test_step.dependOn(&run_unit_tests.step);
}
{
const cli = b.addExecutable(.{
.name = "artificer",
.root_source_file = b.path("cli/main.zig"),
.target = target,
.optimize = optimize,
});
cli.root_module.addImport("artificer", lib);
b.installArtifact(cli);
const run_cmd = b.addRunArtifact(cli);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run-cli", "Run the CLI");
run_step.dependOn(&run_cmd.step);
}
{
const raylib_dep = b.dependency("raylib-zig", .{
.target = target,
.optimize = optimize,
});
const gui = b.addExecutable(.{
.name = "artificer-gui",
.root_source_file = b.path("gui/main.zig"),
.target = target,
.optimize = optimize,
});
gui.root_module.addImport("artificer", lib);
gui.linkLibrary(raylib_dep.artifact("raylib"));
gui.root_module.addImport("raylib", raylib_dep.module("raylib"));
b.installArtifact(gui);
const run_cmd = b.addRunArtifact(gui);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run-gui", "Run the GUI");
run_step.dependOn(&run_cmd.step);
}
}

View File

@ -2,6 +2,11 @@
.name = "artificer",
.version = "0.1.0",
.minimum_zig_version = "0.12.0",
.dependencies = .{ },
.dependencies = .{
.@"raylib-zig" = .{
.url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz",
.hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212"
},
},
.paths = .{ "" },
}

42
cli/main.zig Normal file
View File

@ -0,0 +1,42 @@
const std = @import("std");
const Artificer = @import("artificer");
const Allocator = std.mem.Allocator;
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 "));
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
var artificer = try Artificer.init(allocator, token);
defer artificer.deinit();
std.log.info("Starting main loop", .{});
while (true) {
const waitUntil = artificer.nextStepAt();
const duration = waitUntil - std.time.timestamp();
if (duration > 0) {
std.time.sleep(@intCast(duration));
}
try artificer.step();
}
}

18
gui/main.zig Normal file
View File

@ -0,0 +1,18 @@
const rl = @import("raylib");
pub fn main() anyerror!void {
rl.initWindow(800, 450, "Artificer");
defer rl.closeWindow();
rl.setTargetFPS(60);
while (!rl.windowShouldClose()) {
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(rl.Color.white);
rl.drawText("Congrats! You created your first window!", 190, 200, 20, rl.Color.light_gray);
}
}

View File

@ -1,9 +1,12 @@
const std = @import("std");
const Server = @import("./api/server.zig");
const ArtifactsAPI = @import("artifacts-api");
const Server = ArtifactsAPI.Server;
const Allocator = std.mem.Allocator;
const Position = Server.Position;
const assert = std.debug.assert;
const CharacterTask = @import("./task.zig").Task;
const CharacterBrain = @This();
const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
@ -98,38 +101,6 @@ comptime {
assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len);
}
pub const CharacterTask = union(enum) {
fight: struct {
at: Position,
target: Server.ItemIdQuantity,
progress: u64 = 0,
},
gather: struct {
at: Position,
target: Server.ItemIdQuantity,
progress: u64 = 0,
},
craft: struct {
at: Position,
target: Server.ItemIdQuantity,
progress: u64 = 0,
},
pub fn isComplete(self: CharacterTask) bool {
return switch (self) {
.fight => |args| {
return args.progress >= args.target.quantity;
},
.gather => |args| {
return args.progress >= args.target.quantity;
},
.craft => |args| {
return args.progress >= args.target.quantity;
}
};
}
};
name: []const u8,
action_queue: std.ArrayList(QueuedAction),
task: ?*CharacterTask = null,
@ -397,7 +368,7 @@ fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void {
const fight_result: Server.FightResult = r;
const drops = fight_result.fight.drops;
args.progress += drops.getQuantity(args.target.id);
args.progress += drops.getQuantity(args.until.item.id);
}
},
.gather => |*args| {
@ -405,7 +376,7 @@ fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void {
const gather_resutl: Server.GatherResult = r;
const items = gather_resutl.details.items;
args.progress += items.getQuantity(args.target.id);
args.progress += items.getQuantity(args.until.item.id);
}
},
.craft => |*args| {

134
lib/root.zig Normal file
View File

@ -0,0 +1,134 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
const CharacterBrain = @import("./brain.zig");
const Artificer = @This();
const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms
const server_down_retry_interval = 5; // minutes
server: Api.Server,
characters: std.ArrayList(CharacterBrain),
paused_until: ?i64 = null,
pub fn init(allocator: Allocator, token: []const u8) !Artificer {
var server = try Api.Server.init(allocator);
errdefer server.deinit();
try server.setToken(token);
var characters = std.ArrayList(CharacterBrain).init(allocator);
defer characters.deinit();
// TODO: Add character deinit
// const chars = try api.listMyCharacters();
// defer chars.deinit();
// for (chars.items) |char| {
// try scheduler.addCharacter(char.name);
// }
return Artificer{
.server = server,
.characters = characters
};
}
pub fn deinit(self: Artificer) void {
for (self.characters.items) |brain| {
brain.deinit();
}
self.characters.deinit();
}
pub fn step(self: *Artificer) !void {
if (self.paused_until) |paused_until| {
if (std.time.timestamp() < paused_until) {
return;
}
self.paused_until = null;
}
runNextActions(self.characters.items, &self.server) catch |err| switch (err) {
Api.Error.ServerUnavailable => {
self.paused_until = std.time.timestamp() + std.time.ns_per_min * server_down_retry_interval;
std.log.warn("Server is down, retrying in {}min", .{ server_down_retry_interval });
return;
},
else => return err
};
for (self.characters.items) |*brain| {
if (brain.action_queue.items.len > 0) continue;
if (brain.isTaskFinished()) {
if (try brain.depositItemsToBank(&self.server)) {
continue;
}
brain.task = null;
}
if (brain.task == null) {
// var next_tasks = try task_tree.listNextTasks();
// defer next_tasks.deinit();
//
// if (next_tasks.items.len > 0) {
// const next_task_index = random.intRangeLessThan(usize, 0, next_tasks.items.len);
// brain.task = next_tasks.items[next_task_index];
// }
}
try brain.performTask(&self.server);
}
}
pub fn nextStepAt(self: *Artificer) i64 {
if (self.paused_until) |paused_until| {
return paused_until;
}
return earliestCooldown(self.characters.items, &self.server) orelse 0;
}
fn earliestCooldown(characters: []CharacterBrain, api: *Api.Server) ?i64 {
var earliest_cooldown: ?i64 = null;
for (characters) |*brain| {
if (brain.action_queue.items.len == 0) continue;
const character = api.findCharacter(brain.name).?;
const cooldown: i64 = @intFromFloat(character.cooldown_expiration * std.time.ns_per_s);
if (earliest_cooldown == null or earliest_cooldown.? > cooldown) {
earliest_cooldown = cooldown;
}
}
return earliest_cooldown;
}
fn runNextActions(characters: []CharacterBrain, api: *Api.Server) !void {
const maybe_earliest_cooldown = earliestCooldown(characters, api);
if (maybe_earliest_cooldown == null) return;
const earliest_cooldown = maybe_earliest_cooldown.?;
if (earliest_cooldown < std.time.timestamp()) return;
for (characters) |*brain| {
if (brain.action_queue.items.len == 0) continue;
const character = api.findCharacter(brain.name).?;
const cooldown: u64 = @intFromFloat(character.cooldown_expiration * std.time.ns_per_s);
if (earliest_cooldown > cooldown) {
try brain.performNextAction(api);
}
}
}
fn currentTime() f64 {
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
return timestamp / std.time.ms_per_s;
}

39
lib/task.zig Normal file
View File

@ -0,0 +1,39 @@
const Api = @import("artifacts-api");
const Position = Api.Position;
pub const UntilCondition = union(enum) {
xp: u64,
item: Api.ItemIdQuantity,
};
pub const Task = union(enum) {
fight: struct {
at: Position,
until: UntilCondition,
progress: u64 = 0,
},
gather: struct {
at: Position,
until: UntilCondition,
progress: u64 = 0,
},
craft: struct {
at: Position,
target: Api.ItemIdQuantity,
progress: u64 = 0,
},
pub fn isComplete(self: Task) bool {
return switch (self) {
.fight => |args| {
return args.progress >= args.until.item.quantity;
},
.gather => |args| {
return args.progress >= args.until.item.quantity;
},
.craft => |args| {
return args.progress >= args.target.quantity;
}
};
}
};

234
lib/task_graph.zig Normal file
View File

@ -0,0 +1,234 @@
const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
const TaskGraph = @This();
const CharacterTask = @import("./task.zig").Task;
const TaskNodeId = u16;
const TaskNode = struct {
const Dependencies = std.BoundedArray(TaskNodeId, 8);
const MissingItems = Api.BoundedSlotsArray(8);
task: CharacterTask,
dependencies: Dependencies = Dependencies.init(0) catch unreachable,
missing_items: MissingItems = MissingItems.init(),
pub fn format(
self: TaskNode,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = options;
_ = fmt;
switch (self.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);
// }
// for (node.additional_items.slots.constSlice()) |slot| {
// const item_code = self.api.getItemCode(slot.id).?;
// try writer.writeBytesNTimes(" ", level+1);
// try writer.print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity});
// }
}
};
const Nodes = std.ArrayList(TaskNode);
nodes: Nodes,
pub fn init(allocator: Allocator) TaskGraph {
return TaskGraph{ .nodes = Nodes.init(allocator) };
}
pub fn deinit(self: TaskGraph) void {
self.nodes.deinit();
}
fn get(self: *TaskGraph, id: TaskNodeId) *TaskNode {
return &self.nodes.items[id];
}
fn addTask(self: *TaskGraph, node: TaskNode) !TaskNodeId {
try self.nodes.append(node);
return @intCast(self.nodes.items.len-1);
}
fn addFightTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId {
const item_code = api.getItemCode(item_id) orelse return error.ItemNotFound;
const monsters = try api.getMonsters(.{ .drop = item_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();
// This monster currently doesn't exist on the map. Probably only spawns in certain situations.
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];
return try self.addTask(TaskNode{
.task = .{
.fight = .{
.at = resource_map.position,
.until = .{ .item = Api.ItemIdQuantity.init(item_id, quantity) }
}
}
});
}
fn addGatherTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId {
const item_code = api.getItemCode(item_id) orelse return error.ItemNotFound;
const resources = try api.getResources(.{ .drop = item_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];
return try self.addTask(TaskNode{
.task = .{
.gather = .{
.at = resource_map.position,
.until = .{ .item = Api.ItemIdQuantity.init(item_id, quantity) }
}
}
});
}
fn addCraftTaskShallow(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId {
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
const recipe = item.craft orelse return error.RecipeNotFound;
const skill_str = Api.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", .{});
return try self.addTask(TaskNode{
.task = .{
.craft = .{
.at = workshop_maps.items[0].position,
.target = Api.ItemIdQuantity.init(item_id, quantity)
}
}
});
}
fn addCraftTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) !TaskNodeId {
const node_id = try self.addCraftTaskShallow(api, item_id, quantity);
var node = self.get(node_id);
const item = (try api.getItemById(item_id)) orelse return error.ItemNotFound;
const recipe = item.craft orelse return error.RecipeNotFound;
const craft_count = recipe.quantity;
for (recipe.items.slots.constSlice()) |material| {
const needed_quantity = material.quantity * craft_count;
if (try self.addAutoTask(api, material.id, needed_quantity)) |dependency_id| {
try node.dependencies.append(dependency_id);
} else {
try node.missing_items.add(material.id, needed_quantity);
}
}
return node_id;
}
// TODO: Remove `anyerror` from function declaration
fn addAutoTask(self: *TaskGraph, api: *Api.Server, item_id: Api.ItemId, quantity: u64) anyerror!?TaskNodeId {
const item = (try self.api.getItemById(item_id)) orelse return error.ItemNotFound;
if (item.craft != null) {
return try self.addCraftTask(api, item_id, quantity);
} else if (item.type == .resource) {
const eql = std.mem.eql;
if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) {
return try self.addGatherTask(api, item_id, quantity);
} else if (eql(u8, item.subtype, "mob")) {
return try self.addFightTask(api, item_id, quantity);
}
}
return null;
}
fn printTask(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId) void {
self.printTaskLevel(api, node_id, 0);
}
fn writeIdentation(level: u32) void {
const mutex = std.debug.getStderrMutex();
mutex.lock();
defer mutex.unlock();
const stderr = std.io.getStdErr().writer();
stderr.writeBytesNTimes(" ", level) catch return;
}
fn printTaskLevel(self: *TaskGraph, api: *Api.Server, node_id: TaskNodeId, level: u32) void {
const node = self.get(node_id);
const print = std.debug.print;
writeIdentation(level);
switch (node.task) {
.fight => |args| {
const target_item = args.until.item;
const item = api.getItemCode(target_item.id).?;
print("Task{{ .fight = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
},
.gather => |args| {
const target_item = args.until.item;
const item = api.getItemCode(target_item.id).?;
print("Task{{ .gather = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, target_item.quantity, args.at});
},
.craft => |args| {
const item = api.getItemCode(args.target.id).?;
print("Task{{ .craft = {{ \"{s}\" x {} }}, .at = {} }}\n", .{item, args.target.quantity, args.at});
},
}
for (node.dependencies.constSlice()) |dependency| {
self.printTaskLevel(dependency, level + 1);
}
for (node.missing_items.slots.constSlice()) |slot| {
const item_code = api.getItemCode(slot.id).?;
writeIdentation(level+1);
print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity});
}
}

View File

@ -1,15 +0,0 @@
const Position = @This();
x: i64,
y: i64,
pub fn init(x: i64, y: i64) Position {
return Position{
.x = x,
.y = y
};
}
pub fn eql(self: Position, other: Position) bool {
return self.x == other.x and self.y == other.y;
}

View File

@ -1,374 +0,0 @@
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");
const BoundedSlotsArray = @import("./api/slot_array.zig").BoundedSlotsArray;
// 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,
additional_items: BoundedSlotsArray(8) = BoundedSlotsArray(8).init(),
};
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) !?usize {
if (quantity == 0) return null;
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).?;
const needed_quantity = recipe_item.quantity * craft_count;
const subtree_id = try self.appendSubTree(recipe_item_code, needed_quantity, node_id);
// The target item can not be currently produced.
if (subtree_id == null) {
var node = &self.nodes.items[node_id];
try node.additional_items.add(recipe_item.id, needed_quantity);
}
}
return node_id;
} else if (item.type == .resource) {
if (eql(u8, item.subtype, "mining") or eql(u8, item.subtype, "fishing") or eql(u8, item.subtype, "woodcutting")) {
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];
return 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();
// This monster currently doesn't exist on the map. Probably only spawns in certain situations.
if (resource_maps.items.len == 0) return null;
if (resource_maps.items.len > 1) std.log.warn("Multiple map locations exist for monster", .{});
const resource_map = resource_maps.items[0];
return try self.appendNode(TaskTreeNode{
.parent = parent,
.character_task = .{
.fight = .{
.at = resource_map.position,
.target = .{ .id = item_id, .quantity = quantity }
}
}
});
}
}
return null;
}
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;
}
fn listNextTasks(self: *const TaskTree) !std.ArrayList(*CharacterBrain.CharacterTask) {
var next_tasks = std.ArrayList(*CharacterBrain.CharacterTask).init(self.allocator);
errdefer next_tasks.deinit();
for (0.., self.nodes.items) |i, *node| {
var child_nodes = try self.listByParent(i);
defer child_nodes.deinit();
var can_be_started = true;
for (child_nodes.items) |child_node_id| {
var child_node = self.nodes.items[child_node_id];
if (!child_node.character_task.isComplete()) {
can_be_started = false;
break;
}
}
if (can_be_started) {
try next_tasks.append(&node.character_task);
}
}
return next_tasks;
}
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);
}
for (node.additional_items.slots.constSlice()) |slot| {
const item_code = self.api.getItemCode(slot.id).?;
try writer.writeBytesNTimes(" ", level+1);
try writer.print("+ {{ \"{s}\" x {} }}\n", .{item_code, slot.quantity});
}
}
};
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();
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
try api.setToken(token);
std.log.info("prefetching static data", .{});
try api.prefetch();
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);
}
var task_tree = TaskTree.init(allocator, &api);
defer task_tree.deinit();
// _ = try task_tree.appendSubTree("wooden_staff", 5, null);
_ = try task_tree.appendSubTree("wooden_shield", 5, null);
std.debug.print("{}", .{task_tree});
var default_prng = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp()));
var random = default_prng.random();
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.isTaskFinished()) {
if (try brain.depositItemsToBank(&api)) {
continue;
}
brain.task = null;
}
if (brain.task == null) {
var next_tasks = try task_tree.listNextTasks();
defer next_tasks.deinit();
if (next_tasks.items.len > 0) {
const next_task_index = random.intRangeLessThan(usize, 0, next_tasks.items.len);
brain.task = next_tasks.items[next_task_index];
}
}
try brain.performTask(&api);
}
}
}