split up into multiple modules
This commit is contained in:
parent
64c4d9ff47
commit
45e32424cb
28
api/position.zig
Normal file
28
api/position.zig
Normal 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
8
api/root.zig
Normal 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;
|
@ -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
110
build.zig
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
42
cli/main.zig
Normal 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
18
gui/main.zig
Normal 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);
|
||||
}
|
||||
}
|
@ -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
134
lib/root.zig
Normal 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
39
lib/task.zig
Normal 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
234
lib/task_graph.zig
Normal 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});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
374
src/main.zig
374
src/main.zig
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user