diff --git a/api/ratelimit.zig b/api/ratelimit.zig new file mode 100644 index 0000000..2bd07d2 --- /dev/null +++ b/api/ratelimit.zig @@ -0,0 +1,83 @@ +// zig fmt: off +const std = @import("std"); + +const RateLimit = @This(); + +const assert = std.debug.assert; + +pub const Category = enum { account_creation, token, data, actions }; + +pub const Timespan = struct { + counter: u32 = 0, + limit: u32, + timer_ms: u64 = 0, +}; + +pub const CategoryArray = std.EnumArray(Category, RateLimit); + +seconds: ?Timespan = null, +minutes: ?Timespan = null, +hours: ?Timespan = null, + +last_update_at_ms: i64 = 0, + +pub fn init(now_ms: i64, limit_per_hour: ?u32, limit_per_minute: ?u32, limit_per_second: ?u32) RateLimit { + var seconds: ?Timespan = null; + if (limit_per_second) |limit| { + seconds = Timespan{ .limit = limit }; + } + + var minutes: ?Timespan = null; + if (limit_per_minute) |limit| { + minutes = Timespan{ .limit = limit }; + } + + var hours: ?Timespan = null; + if (limit_per_hour) |limit| { + hours = Timespan{ .limit = limit }; + } + + return RateLimit{ + .seconds = seconds, + .minutes = minutes, + .hours = hours, + .last_update_at_ms = now_ms + }; +} + +pub fn update_timers(self: *RateLimit, now_ms: i64) void { + const time_passed_ms = now_ms - self.last_update_at_ms; + assert(time_passed_ms >= 0); + + inline for (.{ + .{ &self.seconds, std.time.ms_per_s }, + .{ &self.minutes, std.time.ms_per_min }, + .{ &self.hours, std.time.ms_per_hour }, + }) |tuple| { + const maybe_timespan = tuple[0]; + const timespan_size = tuple[1]; + + if (maybe_timespan.*) |*timespan| { + timespan.timer_ms += @intCast(time_passed_ms); + + const ms_per_request = @divFloor(timespan_size, timespan.limit); + const requests_passed: u32 = @intCast(@divFloor(timespan.timer_ms, ms_per_request)); + timespan.counter -= @min(timespan.counter, requests_passed); + timespan.timer_ms = @mod(timespan.timer_ms, ms_per_request); + } + } + + self.last_update_at_ms = now_ms; +} + +pub fn increment_counters(self: *RateLimit) void { + inline for (.{ + &self.hours, + &self.minutes, + &self.seconds, + }) |maybe_timespan| { + if (maybe_timespan.*) |*timespan| { + timespan.counter += 1; + } + } +} diff --git a/api/root.zig b/api/root.zig index 52a2525..49650c8 100644 --- a/api/root.zig +++ b/api/root.zig @@ -13,6 +13,7 @@ pub const docs_url = api_url ++ "/openapi.json"; pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime; pub const Server = @import("server.zig"); +pub const RateLimit = @import("./ratelimit.zig"); pub const Store = @import("store.zig"); pub const Item = @import("./schemas/item.zig"); pub const Status = @import("./schemas/status.zig"); @@ -29,15 +30,7 @@ pub const UnequipResult = EquipResult; const SkillUsageResult = @import("./schemas/skill_usage_result.zig"); pub const GatherResult = SkillUsageResult; pub const CraftResult = SkillUsageResult; - -// pub const ServerStatus = @import("./schemas/status.zig"); -// pub const Map = @import("./schemas/map.zig"); -// pub const Position = @import("position.zig"); -// pub const BoundedSlotsArray = @import("schemas/slot_array.zig").BoundedSlotsArray; - -// pub const Slot = Server.Slot; -// pub const CodeId = Store.Id; -// pub const ItemQuantity = @import("./schemas/item_quantity.zig"); +pub const Cooldown = @import("./schemas/cooldown.zig"); const errors = @import("errors.zig"); pub const FetchError = errors.FetchError; @@ -49,3 +42,5 @@ pub const BankDepositItemError = errors.BankDepositItemError; pub const BankWithdrawItemError = errors.BankWithdrawItemError; pub const CraftError = errors.CraftError; pub const AcceptTaskError = errors.AcceptTaskError; +pub const EquipError = errors.EquipError; +pub const UnequipError = errors.UnequipError; diff --git a/api/schemas/simple_item.zig b/api/schemas/simple_item.zig index b69fe8c..f0cede4 100644 --- a/api/schemas/simple_item.zig +++ b/api/schemas/simple_item.zig @@ -99,9 +99,9 @@ pub fn BoundedArray(comptime slot_count: u32) type { } } - pub fn addSlice(self: *@This(), items: []const SimpleItem) void { + pub fn addSlice(self: *@This(), items: []const SimpleItem) !void { for (items) |item| { - self.add(item.id, item.quantity); + try self.add(item.id, item.quantity); } } diff --git a/api/schemas/skill_usage_result.zig b/api/schemas/skill_usage_result.zig index fd95c4f..155f04f 100644 --- a/api/schemas/skill_usage_result.zig +++ b/api/schemas/skill_usage_result.zig @@ -8,8 +8,10 @@ const SkillInfoDetails = @import("./skill_info_details.zig"); const SkillUsageResult = @This(); +pub const Details = SkillInfoDetails; + cooldown: Cooldown, -details: SkillInfoDetails, +details: Details, character: Character, fn parse(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult { diff --git a/api/server.zig b/api/server.zig index bbb823e..54e0b73 100644 --- a/api/server.zig +++ b/api/server.zig @@ -5,6 +5,7 @@ const std = @import("std"); const json_utils = @import("json_utils.zig"); const errors = @import("./errors.zig"); const stb_image = @import("./stb_image/root.zig"); +const RateLimit = @import("./ratelimit.zig"); const FetchError = errors.FetchError; const assert = std.debug.assert; @@ -28,108 +29,18 @@ const MoveResult = @import("./schemas/move_result.zig"); const SkillUsageResult = @import("./schemas/skill_usage_result.zig"); const EquipResult = @import("./schemas/equip_result.zig"); const UnequipResult = EquipResult; -pub const GatherResult = SkillUsageResult; -pub const CraftResult = SkillUsageResult; +const GatherResult = SkillUsageResult; +const CraftResult = SkillUsageResult; const Image = Store.Image; const Server = @This(); -pub const RateLimit = struct { - pub const Category = enum { - account_creation, - token, - data, - actions - }; - - pub const Timespan = struct { - counter: u32 = 0, - limit: u32, - timer_ms: u64 = 0, - }; - - seconds: ?Timespan = null, - minutes: ?Timespan = null, - hours: ?Timespan = null, - - last_update_at_ms: i64 = 0, - - pub fn init( - now: i64, - limit_per_hour: ?u32, - limit_per_minute: ?u32, - limit_per_second: ?u32 - ) RateLimit { - var seconds: ?Timespan = null; - if (limit_per_second) |limit| { - seconds = Timespan{ .limit = limit }; - } - - var minutes: ?Timespan = null; - if (limit_per_minute) |limit| { - minutes = Timespan{ .limit = limit }; - } - - var hours: ?Timespan = null; - if (limit_per_hour) |limit| { - hours = Timespan{ .limit = limit }; - } - - return RateLimit{ - .seconds = seconds, - .minutes = minutes, - .hours = hours, - .last_update_at_ms = now - }; - } - - pub fn update_timers(self: *RateLimit) void { - const now = std.time.milliTimestamp(); - const time_passed_ms = now - self.last_update_at_ms; - assert(time_passed_ms >= 0); - - inline for (.{ - .{ &self.seconds, std.time.ms_per_s }, - .{ &self.minutes, std.time.ms_per_min }, - .{ &self.hours , std.time.ms_per_hour }, - }) |tuple| { - const maybe_timespan = tuple[0]; - const timespan_size = tuple[1]; - - if (maybe_timespan.*) |*timespan| { - timespan.timer_ms += @intCast(time_passed_ms); - - const ms_per_request = @divFloor(timespan_size, timespan.limit); - const requests_passed: u32 = @intCast(@divFloor(timespan.timer_ms, ms_per_request)); - timespan.counter -= @min(timespan.counter, requests_passed); - timespan.timer_ms = @mod(timespan.timer_ms, ms_per_request); - } - } - - self.last_update_at_ms = now; - } - - pub fn increment_counters(self: *RateLimit) void { - inline for (.{ - &self.hours, - &self.minutes, - &self.seconds, - }) |maybe_timespan| { - if (maybe_timespan.*) |*timespan| { - timespan.counter += 1; - } - } - } -}; - -const RateLimits = std.EnumArray(RateLimit.Category, RateLimit); - const max_response_size = 1024 * 1024 * 16; // TODO: Figure out a way to more accuretly pick a good 'self.fetch_buffer' size fetch_buffer: []u8, client: std.http.Client, -ratelimits: RateLimits, +ratelimits: RateLimit.CategoryArray, server_url: ServerURL, server_uri: std.Uri, @@ -144,7 +55,7 @@ pub fn init(allocator: Allocator, store: *Store) !Server { const now = std.time.milliTimestamp(); // Limits gotten from https://docs.artifactsmmo.com/api_guide/rate_limits - var ratelimits = RateLimits.initFill(RateLimit{}); + var ratelimits = RateLimit.CategoryArray.initFill(RateLimit{}); ratelimits.set(.account_creation, RateLimit.init(now, 50, null, null)); ratelimits.set(.token, RateLimit.init(now, 50, null, null)); ratelimits.set(.data, RateLimit.init(now, 7200, 200, 16)); @@ -235,11 +146,14 @@ fn fetch(self: *Server, ratelimit: RateLimit.Category, options: std.http.Client. log.debug("| payload {s}", .{ payload }); } + const started_at = std.time.nanoTimestamp(); const result = try self.client.fetch(options); + const duration_ns = std.time.nanoTimestamp() - started_at; var ratelimit_obj = self.ratelimits.getPtr(ratelimit); ratelimit_obj.increment_counters(); + log.debug("| duration {d:.3}s", .{ @as(f64, @floatFromInt(duration_ns)) / std.time.ns_per_s }); log.debug("| status {}", .{ result.status }); if (ratelimit != .token) { diff --git a/api/store.zig b/api/store.zig index 6ea58d1..ebc9bfe 100644 --- a/api/store.zig +++ b/api/store.zig @@ -140,6 +140,51 @@ const Images = struct { } } + pub fn clone(self: *Images, allocator: std.mem.Allocator) !Images { + const rgba_data = try allocator.dupe(self.rgba_data); + errdefer allocator.free(rgba_data); + + var images = try self.images.clone(allocator); + errdefer images.deinit(allocator); + + var character_images = try self.category_mapping.get(.character).clone(allocator); + errdefer character_images.deinit(allocator); + + var item_images = try self.category_mapping.get(.item).clone(allocator); + errdefer item_images.deinit(allocator); + + var monster_images = try self.category_mapping.get(.monster).clone(allocator); + errdefer monster_images.deinit(allocator); + + var map_images = try self.category_mapping.get(.map).clone(allocator); + errdefer map_images.deinit(allocator); + + var resource_images = try self.category_mapping.get(.resource).clone(allocator); + errdefer resource_images.deinit(allocator); + + var effect_images = try self.category_mapping.get(.effect).clone(allocator); + errdefer effect_images.deinit(allocator); + + const category_mapping = CategoryMap.init(.{ + .character = character_images, + .item = item_images, + .monster = monster_images, + .resource = resource_images, + .effect = effect_images, + .map = map_images + }); + + return Images{ + .rgba_data = rgba_data, + .rgba_fba = std.heap.FixedBufferAllocator{ + .buffer = rgba_data, + .end_index = self.rgba_fba.end_index + }, + .images = images, + .category_mapping = category_mapping + }; + } + pub fn get(self: *Images, id: Id) ?*Image { if (id < self.images.items.len) { return &self.images.items[id]; @@ -217,6 +262,12 @@ fn Repository(comptime Object: type, comptime name_field: []const u8) type { }; } + pub fn clone(self: @This(), allocator: std.mem.Allocator) !@This() { + return @This(){ + .objects = try self.objects.clone(allocator) + }; + } + pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { self.objects.deinit(allocator); } @@ -387,6 +438,44 @@ pub fn deinit(self: *Store, allocator: std.mem.Allocator) void { self.ge_orders.deinit(allocator); } +pub fn clone(self: *Store, allocator: std.mem.Allocator) !Store { + var items = try self.items.clone(allocator); + errdefer items.deinit(allocator); + + var characters = try self.characters.clone(allocator); + errdefer characters.deinit(allocator); + + var tasks = try self.tasks.clone(allocator); + errdefer tasks.deinit(allocator); + + var monsters = try self.monsters.clone(allocator); + errdefer monsters.deinit(allocator); + + var resources = try self.resources.clone(allocator); + errdefer resources.deinit(allocator); + + var ge_orders = try self.ge_orders.clone(allocator); + errdefer ge_orders.deinit(allocator); + + var maps = try self.maps.clone(allocator); + errdefer maps.deinit(allocator); + + var images = try self.images.clone(allocator); + errdefer images.deinit(allocator); + + return Store{ + .items = items, + .characters = characters, + .tasks = tasks, + .monsters = monsters, + .resources = resources, + .ge_orders = ge_orders, + .maps = maps, + .bank = self.bank, + .images = images + }; +} + const SaveData = struct { api_version: []const u8, diff --git a/cli/main.zig b/cli/main.zig index 5080abb..7ff0179 100644 --- a/cli/main.zig +++ b/cli/main.zig @@ -6,6 +6,8 @@ const Allocator = std.mem.Allocator; const Artificer = @import("artificer"); const Api = @import("artifacts-api"); +const simulated = true; + pub const std_options = .{ .log_scope_levels = &[_]std.log.ScopeLevel{ .{ .scope = .api, .level = .info }, @@ -58,16 +60,33 @@ pub fn main() !void { const character_id = (try server.getCharacter("Blondie")).?; - var artificer = try Artificer.init(allocator, &server, character_id); + var system_clock = Artificer.SystemClock{}; + var sim_server = Artificer.SimServer.init(0, &store); + + if (simulated) { + const character = store.characters.get(character_id).?; + character.cooldown_expiration = null; + } + + var artificer = if (simulated) + try Artificer.ArtificerSim.init(allocator, &store, &sim_server.clock, &sim_server, character_id) + else + try Artificer.ArtificerApi.init(allocator, &store, &system_clock, &server, character_id); + defer artificer.deinit(allocator); - _ = try artificer.appendGoal(Artificer.Goal{ - .equip = .{ - .slot = .weapon, - .item = store.items.getId("copper_dagger").? + _ = try artificer.appendGoal(.{ + .gather = .{ + .item = store.items.getId("copper_ore").?, + .quantity = 3 } }); std.log.info("Starting main loop", .{}); + const started_at = artificer.clock.nanoTimestamp(); try artificer.runUntilGoalsComplete(); + const stopped_at = artificer.clock.nanoTimestamp(); + + const elapsed_time = @as(f64, @floatFromInt(stopped_at - started_at)) / std.time.ns_per_s; + std.log.info("Took {d:.3}s", .{ elapsed_time }); } diff --git a/gui/app.zig b/gui/app.zig index b3432d5..043e1b8 100644 --- a/gui/app.zig +++ b/gui/app.zig @@ -21,7 +21,8 @@ const MapTexture = struct { }; ui: UI, -artificer: *Artificer, +server: *Api.Server, +store: *Api.Store, map_textures: std.ArrayList(MapTexture), map_texture_indexes: std.ArrayList(usize), map_position_min: Api.Position, @@ -34,9 +35,7 @@ blur_texture_horizontal: ?rl.RenderTexture = null, blur_texture_both: ?rl.RenderTexture = null, blur_shader: rl.Shader, -pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App { - const store = artificer.server.store; - +pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server) !App { var map_textures = std.ArrayList(MapTexture).init(allocator); errdefer map_textures.deinit(); errdefer { @@ -126,7 +125,8 @@ pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App { } return App{ - .artificer = artificer, + .store = store, + .server = server, .ui = UI.init(), .map_textures = map_textures, .map_texture_indexes = map_texture_indexes, @@ -598,8 +598,8 @@ pub fn drawWorldAndBlur(self: *App) !void { } fn drawRatelimits(self: *App, box: rl.Rectangle) void { - const Category = Api.Server.RateLimit.Category; - const ratelimits = self.artificer.server.ratelimits; + const Category = Api.RateLimit.Category; + const ratelimits = self.server.ratelimits; self.drawBlurredWorld( box, @@ -664,10 +664,8 @@ fn drawRatelimits(self: *App, box: rl.Rectangle) void { } pub fn tick(self: *App) !void { - var server = self.artificer.server; - try self.artificer.tick(); - for (&server.ratelimits.values) |*ratelimit| { - ratelimit.update_timers(); + for (&self.server.ratelimits.values) |*ratelimit| { + ratelimit.update_timers(std.time.milliTimestamp()); } const screen_width = rl.getScreenWidth(); diff --git a/gui/main.zig b/gui/main.zig index cee1bb4..4b2c2ab 100644 --- a/gui/main.zig +++ b/gui/main.zig @@ -1,6 +1,5 @@ // zig fmt: off const std = @import("std"); -const Artificer = @import("artificer"); const Api = @import("artifacts-api"); const rl = @import("raylib"); const raylib_h = @cImport({ @@ -11,7 +10,7 @@ const App = @import("./app.zig"); pub const std_options = .{ .log_scope_levels = &[_]std.log.ScopeLevel{ - .{ .scope = .api, .level = .info }, + .{ .scope = .api, .level = .warn }, .{ .scope = .raylib, .level = .warn }, } }; @@ -94,9 +93,6 @@ pub fn main() anyerror!void { try server.setToken(token); - var artificer = try Artificer.init(allocator, &server); - defer artificer.deinit(); - std.log.info("Prefetching server data", .{}); { const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); @@ -117,7 +113,7 @@ pub fn main() anyerror!void { .window_resizable = true }); - var app = try App.init(allocator, &artificer); + var app = try App.init(allocator, &store, &server); defer app.deinit(); while (!rl.windowShouldClose()) { diff --git a/lib/artificer.zig b/lib/artificer.zig new file mode 100644 index 0000000..b00c1fb --- /dev/null +++ b/lib/artificer.zig @@ -0,0 +1,457 @@ +// zig fmt: off +const std = @import("std"); +const Api = @import("artifacts-api"); +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; + } + + 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(); + } + } + }; +} diff --git a/lib/craft_goal.zig b/lib/craft_goal.zig index ab80f37..73d9b71 100644 --- a/lib/craft_goal.zig +++ b/lib/craft_goal.zig @@ -1,6 +1,8 @@ // zig fmt: off const Api = @import("artifacts-api"); -const Artificer = @import("./root.zig"); + +const Artificer = @import("./artificer.zig"); +const Context = Artificer.GoalContext; const Requirements = Artificer.Requirements; const Goal = @This(); @@ -15,12 +17,11 @@ fn getCraftMultiples(self: Goal, craft: Api.Craft) u64 { )); } -pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void { - const store = artificer.server.store; - const character = store.characters.get(artificer.character).?; +pub fn tick(self: *Goal, ctx: *Context) !void { + const store = ctx.store; if (self.quantity == 0) { - artificer.removeGoal(goal_id); + ctx.completed = true; return; } @@ -28,29 +29,27 @@ pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void const craft = item.craft.?; const skill = craft.skill.toCharacterSkill(); - if (character.skills.get(skill).level < craft.level) { + if (ctx.character.skills.get(skill).level < craft.level) { return error.SkillTooLow; } - const workshop_position = artificer.findNearestWorkstation(craft.skill).?; - if (!workshop_position.eql(character.position)) { - artificer.queued_actions.appendAssumeCapacity(.{ - .goal = goal_id, - .action = .{ .move = workshop_position } + const workshop_position = ctx.findNearestWorkstation(craft.skill).?; + if (!workshop_position.eql(ctx.character.position)) { + ctx.queueAction(.{ + .move = workshop_position }); return; } - artificer.queued_actions.appendAssumeCapacity(.{ - .goal = goal_id, - .action = .{ .craft = .{ .item = self.item, .quantity = self.quantity } } + ctx.queueAction(.{ + .craft = .{ .item = self.item, .quantity = self.quantity } }); } -pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements { - var reqs: Artificer.Requirements = .{}; +pub fn requirements(self: Goal, ctx: *Context) Requirements { + var reqs: Requirements = .{}; - const store = artificer.server.store; + const store = ctx.store; const item = store.items.get(self.item).?; const craft = item.craft.?; const craft_multiples = self.getCraftMultiples(craft); @@ -62,8 +61,8 @@ pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements { return reqs; } -pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void { - _ = goal_id; +pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void { + _ = ctx; if (result == .craft) { const craft_result = result.craft; diff --git a/lib/equip_goal.zig b/lib/equip_goal.zig index 4082842..b4418da 100644 --- a/lib/equip_goal.zig +++ b/lib/equip_goal.zig @@ -1,7 +1,10 @@ // zig fmt: off const std = @import("std"); const Api = @import("artifacts-api"); -const Artificer = @import("./root.zig"); + +const Artificer = @import("./artificer.zig"); +const Context = Artificer.GoalContext; +const Requirements = Artificer.Requirements; const Goal = @This(); @@ -9,53 +12,45 @@ slot: Api.Character.Equipment.SlotId, item: Api.Store.Id, quantity: u64 = 1, -pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) void { - const store = artificer.server.store; - const character = store.characters.get(artificer.character).?; +pub fn tick(self: *Goal, ctx: *Context) void { + const character = ctx.character; const equipment_slot = character.equipment.slots.get(self.slot); if (equipment_slot.item) |equiped_item|{ if (equiped_item == self.item and !self.slot.canHoldManyItems()) { - artificer.removeGoal(goal_id); + ctx.completed = true; } else { - artificer.queued_actions.appendAssumeCapacity(.{ - .goal = goal_id, - .action = .{ - .unequip = .{ - .slot = self.slot, - .quantity = self.quantity - } + ctx.queueAction(.{ + .unequip = .{ + .slot = self.slot, + .quantity = self.quantity } }); } return; } - artificer.queued_actions.appendAssumeCapacity(.{ - .goal = goal_id, - .action = .{ - .equip = .{ - .slot = self.slot, - .item = self.item, - .quantity = self.quantity - } + ctx.queueAction(.{ + .equip = .{ + .slot = self.slot, + .item = self.item, + .quantity = self.quantity } }); } -pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements { - _ = artificer; +pub fn requirements(self: Goal, ctx: *Context) Requirements { + _ = ctx; var reqs: Artificer.Requirements = .{}; reqs.items.addAssumeCapacity(self.item, self.quantity); - return reqs; } -pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer, result: Artificer.ActionResult) void { +pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void { _ = self; if (result == .equip) { - artificer.removeGoal(goal_id); + ctx.completed = true; } } diff --git a/lib/gather_goal.zig b/lib/gather_goal.zig index 712003d..e9ff651 100644 --- a/lib/gather_goal.zig +++ b/lib/gather_goal.zig @@ -1,50 +1,49 @@ // zig fmt: off const Api = @import("artifacts-api"); -const Artificer = @import("./root.zig"); + +const Artificer = @import("./artificer.zig"); +const Context = Artificer.GoalContext; +const Requirements = Artificer.Requirements; const Goal = @This(); item: Api.Store.Id, quantity: u64, -pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) void { - const store = artificer.server.store; - const character = store.characters.get(artificer.character).?; - +pub fn tick(self: *Goal, ctx: *Context) void { if (self.quantity == 0) { - artificer.removeGoal(goal_id); + ctx.completed = true; return; } - const resource_id = artificer.findBestResourceWithItem(self.item) orelse @panic("Failed to find resource with item"); + const resource_id = ctx.findBestResourceWithItem(self.item) orelse @panic("Failed to find resource with item"); - const map_position: Api.Position = artificer.findNearestMapWithResource(resource_id) orelse @panic("Map with resource not found"); + const map_position: Api.Position = ctx.findNearestMapWithResource(resource_id) orelse @panic("Map with resource not found"); - if (!map_position.eql(character.position)) { - artificer.queued_actions.appendAssumeCapacity(.{ - .goal = goal_id, - .action = .{ .move = map_position } + if (!map_position.eql(ctx.character.position)) { + ctx.queueAction(.{ + .move = map_position }); return; } - artificer.queued_actions.appendAssumeCapacity(.{ - .goal = goal_id, - .action = .{ .gather = {} } + // TODO: Check for enough space in invetory? Or add it as a requirement + ctx.queueAction(.{ + .gather = {} }); } -pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements { +pub fn requirements(self: Goal, ctx: *Context) Requirements { + _ = ctx; _ = self; - _ = artificer; - const reqs: Artificer.Requirements = .{}; + const reqs: Requirements = .{}; // TODO: add skill requirement return reqs; } -pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void { - _ = goal_id; +pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void { + _ = ctx; if (result == .gather) { const gather_result = result.gather; diff --git a/lib/root.zig b/lib/root.zig index 84ed827..c3b370d 100644 --- a/lib/root.zig +++ b/lib/root.zig @@ -1,420 +1,11 @@ // zig fmt: off -const std = @import("std"); const Api = @import("artifacts-api"); -const Allocator = std.mem.Allocator; -const GatherGoal = @import("gather_goal.zig"); -const CraftGoal = @import("craft_goal.zig"); -const EquipGoal = @import("equip_goal.zig"); +pub const ArtificerType = @import("./artificer.zig").ArtificerType; +pub const SimServer = @import("./sim_server.zig"); +pub const SimClock = @import("./sim_clock.zig"); +pub const SystemClock = @import("./system_clock.zig"); -const assert = std.debug.assert; -const log = std.log.scoped(.artificer); +pub const ArtificerApi = ArtificerType(SystemClock, Api.Server); +pub const ArtificerSim = ArtificerType(SimClock, SimServer); -const Artificer = @This(); - -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, goal_id: Artificer.GoalId, artificer: *Artificer) !void { - switch (self.*) { - .gather => |*gather| gather.tick(goal_id, artificer), - .craft => |*craft| try craft.tick(goal_id, artificer), - .equip => |*equip| equip.tick(goal_id, artificer), - } - } - - pub fn requirements(self: Goal, artificer: *Artificer) Requirements { - return switch (self) { - .gather => |gather| gather.requirements(artificer), - .craft => |craft| craft.requirements(artificer), - .equip => |equip| equip.requirements(artificer), - }; - } - - pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer, result: Artificer.ActionResult) void { - switch (self.*) { - .gather => |*gather| gather.onActionCompleted(goal_id, result), - .craft => |*craft| craft.onActionCompleted(goal_id, result), - .equip => |*equip| equip.onActionCompleted(goal_id, artificer, 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); - -server: *Api.Server, -character: Api.Store.Id, -goal_slots: []GoalSlot, -queued_actions: QueuedActions, - -pub fn init(allocator: Allocator, server: *Api.Server, character: Api.Store.Id) !Artificer { - 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 Artificer{ - .server = server, - .goal_slots = goal_slots, - .character = character, - .queued_actions = queued_actions - }; -} - -pub fn deinit(self: *Artificer, allocator: Allocator) void { - allocator.free(self.goal_slots); - self.queued_actions.deinit(allocator); -} - -pub fn appendGoal(self: *Artificer, 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: *Artificer, id: GoalId) void { - if (self.getGoal(id)) |goal_slot| { - goal_slot.* = .{ - .generation = goal_slot.generation + 1 - }; - } -} - -pub fn getGoal(self: *Artificer, 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 findBestResourceWithItem(self: *Artificer, item: Api.Store.Id) ?Api.Store.Id { - const store = self.server.store; - const character = store.characters.get(self.character).?; - - var best_resource: ?Api.Store.Id = null; - var best_rate: u64 = 0; - - for (0.., 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 = 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: *Artificer, resource: Api.Store.Id) ?Api.Position { - const store = self.server.store; - const character = store.characters.get(self.character).?; - const resource_code = store.resources.get(resource).?.code.slice(); - - var nearest_position: ?Api.Position = null; - for (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(character.position) < map.position.distance(nearest_position.?)) { - nearest_position = map.position; - } - } - - return nearest_position; -} - -pub fn findNearestWorkstation(self: *Artificer, skill: Api.Craft.Skill) ?Api.Position { - const store = self.server.store; - const character = store.characters.get(self.character).?; - const skill_name = skill.toString(); - - var nearest_position: ?Api.Position = null; - for (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(character.position) < map.position.distance(nearest_position.?)) { - nearest_position = map.position; - } - } - - return nearest_position; -} - -fn timeUntilCooldownExpires(self: *Artificer) 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 = std.time.nanoTimestamp(); - if (cooldown_expiration_ns > now) { - return @intCast(@as(i128, @intCast(cooldown_expiration_ns)) - now); - } - } - - return 0; -} - -fn getGoalCount(self: *Artificer) u32 { - var count: u32 = 0; - - for (self.goal_slots) |goal_slot| { - if (goal_slot.goal == null) { - continue; - } - - count += 1; - } - - return count; -} - -fn hasSubGoals(self: *Artificer, 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; -} - -pub fn tick(self: *Artificer) !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(); - 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.?; - goal.onActionCompleted(action_slot.goal, self, action_result); - } - - } 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; - } - - const reqs = goal.requirements(self); - 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 (self.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_id, self); - } - } -} - -pub fn runUntilGoalsComplete(self: *Artificer) !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 }); - std.time.sleep(expires_in); - } - - try self.tick(); - } -} - -pub fn runForever(self: *Artificer) !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 }); - std.time.sleep(expires_in); - log.debug("Finished sleeping", .{}); - } - - try self.tick(); - } -} diff --git a/lib/sim_clock.zig b/lib/sim_clock.zig new file mode 100644 index 0000000..3302900 --- /dev/null +++ b/lib/sim_clock.zig @@ -0,0 +1,13 @@ +// zig fmt: off +const std = @import("std"); +const Clock = @This(); + +timestamp: i128 = 0, + +pub fn sleep(self: *Clock, nanoseconds: u64) void { + self.timestamp += @intCast(nanoseconds); +} + +pub fn nanoTimestamp(self: Clock) i128 { + return self.timestamp; +} diff --git a/lib/sim_server.zig b/lib/sim_server.zig new file mode 100644 index 0000000..8478707 --- /dev/null +++ b/lib/sim_server.zig @@ -0,0 +1,227 @@ +// zig fmt: off +const std = @import("std"); +const Api = @import("artifacts-api"); +const SimClock = @import("./sim_clock.zig"); +const Server = @This(); + +const max_level = 40; + +// https://docs.artifactsmmo.com/concepts/skills#experience-to-level +const max_xp_per_level = [max_level]u64{ + 150, // level 1 + 250, + 350, + 450, + 700, + 950, + 1200, + 1450, + 1700, + 2100, // level 10 + 2500, + 2900, + 3300, + 3700, + 4400, + 5100, + 5800, + 6500, + 7200, + 8200, // level 20 + 9200, + 10200, + 11200, + 12200, + 13400, + 14600, + 15800, + 17000, + 18200, + 19700, // level 30 + 21200, + 22700, + 24200, + 25700, + 27200, + 28700, + 30500, + 32300, + 34100, + 35900, // level 40 +}; + + +store: *Api.Store, +clock: SimClock = .{}, +rng: std.Random.DefaultPrng, + +pub fn init(seed: u64, store: *Api.Store) Server { + return Server{ + .rng = std.Random.DefaultPrng.init(seed), + .store = store + }; +} + +fn sleepNorm(self: *Server, stddev_ns: u64, mean_ns: u64) void { + const stddev_ns_f64: f64 = @floatFromInt(stddev_ns); + const mean_ns_f64: f64 = @floatFromInt(mean_ns); + + var rng = self.rng.random(); + const duration = rng.floatNorm(f64) * stddev_ns_f64 + mean_ns_f64; + self.clock.sleep(@intFromFloat(duration)); +} + +fn sleepRequestBegin(self: *Server) void { + self.sleepNorm( + 100 * std.time.ns_per_ms, + 350 * std.time.ns_per_ms, + ); +} + +fn sleepRequestEnd(self: *Server) void { + self.sleepNorm( + 10 * std.time.ns_per_ms, + 300 * std.time.ns_per_ms, + ); +} + +pub fn move(self: *Server, character_name: []const u8, position: Api.Position) Api.MoveError!Api.MoveResult { + self.sleepRequestBegin(); + defer self.sleepRequestEnd(); + + const map: *Api.Map = self.store.getMap(position) orelse return Api.MoveError.MapNotFound; + + const character_id = self.store.characters.getId(character_name) orelse return Api.MoveError.CharacterNotFound; + const character: *Api.Character = self.store.characters.get(character_id).?; + + if (character.position.eql(position)) { + return Api.MoveError.CharacterAlreadyMap; + } + + const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s; + if (character.cooldown_expiration) |cooldown_expiration| { + if (cooldown_expiration > now) { + return Api.MoveError.CharacterInCooldown; + } + } + + const duration_i64 = (@abs(position.x - character.position.x) + @abs(position.y - character.position.y)) * 5; + const duration_f64: f64 = @floatFromInt(duration_i64); + const expiration = now + duration_f64; + + character.cooldown_expiration = expiration; + character.position = position; + + return Api.MoveResult{ + .cooldown = Api.Cooldown{ + .reason = .movement, + .started_at = now, + .expiration = expiration + }, + .character = character.*, + .destination = map.* + }; +} + +pub fn gather(self: *Server, character_name: []const u8) Api.GatherError!Api.GatherResult { + self.sleepRequestBegin(); + defer self.sleepRequestEnd(); + + const character_id = self.store.characters.getId(character_name) orelse return Api.GatherError.CharacterNotFound; + const character: *Api.Character = self.store.characters.get(character_id).?; + + const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s; + if (character.cooldown_expiration) |cooldown_expiration| { + if (cooldown_expiration > now) { + return Api.GatherError.CharacterInCooldown; + } + } + + const map: *Api.Map = self.store.getMap(character.position) orelse return Api.GatherError.FatalError; + const map_content = map.content orelse return Api.GatherError.MapContentNotFound; + if (map_content.type != .resource) { + return Api.GatherError.MapContentNotFound; + } + + const map_content_code = map_content.code.slice(); + const resource_id = self.store.resources.getId(map_content_code) orelse return Api.GatherError.MapContentNotFound; + const resource = self.store.resources.get(resource_id) orelse return Api.GatherError.FatalError; + + const character_skill = character.skills.getPtr(resource.skill.toCharacterSkill()); + if (character_skill.level < resource.level) { + return Api.GatherError.CharacterNotSkillLevelRequired; + } + + const duration = 25; // TODO: Update duration calculation + const expiration = now + duration; + + var items: Api.GatherResult.Details.Items = .{}; + const xp = 8; // TODO: Update xp calculation + + var rng = self.rng.random(); + for (resource.drops.slice()) |_drop| { + const drop: Api.Resource.Drop = _drop; + if (rng.uintLessThan(u64, drop.rate) == 0) { + const quantity = rng.intRangeAtMost(u64, drop.min_quantity, drop.max_quantity); + items.add(drop.item, quantity) catch return Api.GatherError.FatalError; + } + } + + var inventory = character.inventory; + inventory.addSlice(items.slice()) catch return Api.GatherError.CharacterInventoryFull; + + if (inventory.totalQuantity() > character.inventory_max_items) { + return Api.GatherError.CharacterInventoryFull; + } + character.inventory = inventory; + + character_skill.xp += xp; + while (character_skill.xp > character_skill.max_xp and character_skill.level < max_level) { + character_skill.level += 1; + character_skill.max_xp = max_xp_per_level[character_skill.level - 1]; + } + if (character_skill.level == max_level) { + character_skill.xp = 0; + character_skill.max_xp = 0; + } + + character.cooldown_expiration = expiration; + + return Api.GatherResult{ + .character = character.*, + .cooldown = Api.Cooldown{ + .reason = .gathering, + .started_at = now, + .expiration = expiration + }, + .details = Api.GatherResult.Details{ + .xp = xp, + .items = items + } + }; +} + +pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u64) Api.CraftError!Api.CraftResult { + _ = self; + _ = character; + _ = item; + _ = quantity; + return Api.FetchError.FatalError; +} + +pub fn equip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, item: []const u8, quantity: u64) Api.EquipError!Api.EquipResult { + _ = self; + _ = character; + _ = item; + _ = slot; + _ = quantity; + return Api.FetchError.FatalError; +} + +pub fn unequip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, quantity: u64) Api.EquipError!Api.EquipResult { + _ = self; + _ = character; + _ = slot; + _ = quantity; + return Api.FetchError.FatalError; +} diff --git a/lib/system_clock.zig b/lib/system_clock.zig new file mode 100644 index 0000000..2d7936c --- /dev/null +++ b/lib/system_clock.zig @@ -0,0 +1,12 @@ +const std = @import("std"); +const Clock = @This(); + +pub fn sleep(self: *Clock, nanoseconds: u64) void { + _ = self; + std.time.sleep(nanoseconds); +} + +pub fn nanoTimestamp(self: *Clock) i128 { + _ = self; + return std.time.nanoTimestamp(); +}