create simulated server
This commit is contained in:
parent
4e0f33e2ff
commit
b6859909ab
83
api/ratelimit.zig
Normal file
83
api/ratelimit.zig
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
api/root.zig
13
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 parseDateTime = @import("./date_time/parse.zig").parseDateTime;
|
||||||
|
|
||||||
pub const Server = @import("server.zig");
|
pub const Server = @import("server.zig");
|
||||||
|
pub const RateLimit = @import("./ratelimit.zig");
|
||||||
pub const Store = @import("store.zig");
|
pub const Store = @import("store.zig");
|
||||||
pub const Item = @import("./schemas/item.zig");
|
pub const Item = @import("./schemas/item.zig");
|
||||||
pub const Status = @import("./schemas/status.zig");
|
pub const Status = @import("./schemas/status.zig");
|
||||||
@ -29,15 +30,7 @@ pub const UnequipResult = EquipResult;
|
|||||||
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
|
const SkillUsageResult = @import("./schemas/skill_usage_result.zig");
|
||||||
pub const GatherResult = SkillUsageResult;
|
pub const GatherResult = SkillUsageResult;
|
||||||
pub const CraftResult = SkillUsageResult;
|
pub const CraftResult = SkillUsageResult;
|
||||||
|
pub const Cooldown = @import("./schemas/cooldown.zig");
|
||||||
// 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");
|
|
||||||
|
|
||||||
const errors = @import("errors.zig");
|
const errors = @import("errors.zig");
|
||||||
pub const FetchError = errors.FetchError;
|
pub const FetchError = errors.FetchError;
|
||||||
@ -49,3 +42,5 @@ pub const BankDepositItemError = errors.BankDepositItemError;
|
|||||||
pub const BankWithdrawItemError = errors.BankWithdrawItemError;
|
pub const BankWithdrawItemError = errors.BankWithdrawItemError;
|
||||||
pub const CraftError = errors.CraftError;
|
pub const CraftError = errors.CraftError;
|
||||||
pub const AcceptTaskError = errors.AcceptTaskError;
|
pub const AcceptTaskError = errors.AcceptTaskError;
|
||||||
|
pub const EquipError = errors.EquipError;
|
||||||
|
pub const UnequipError = errors.UnequipError;
|
||||||
|
@ -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| {
|
for (items) |item| {
|
||||||
self.add(item.id, item.quantity);
|
try self.add(item.id, item.quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,10 @@ const SkillInfoDetails = @import("./skill_info_details.zig");
|
|||||||
|
|
||||||
const SkillUsageResult = @This();
|
const SkillUsageResult = @This();
|
||||||
|
|
||||||
|
pub const Details = SkillInfoDetails;
|
||||||
|
|
||||||
cooldown: Cooldown,
|
cooldown: Cooldown,
|
||||||
details: SkillInfoDetails,
|
details: Details,
|
||||||
character: Character,
|
character: Character,
|
||||||
|
|
||||||
fn parse(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
|
fn parse(store: *Store, obj: std.json.ObjectMap) !SkillUsageResult {
|
||||||
|
102
api/server.zig
102
api/server.zig
@ -5,6 +5,7 @@ const std = @import("std");
|
|||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("json_utils.zig");
|
||||||
const errors = @import("./errors.zig");
|
const errors = @import("./errors.zig");
|
||||||
const stb_image = @import("./stb_image/root.zig");
|
const stb_image = @import("./stb_image/root.zig");
|
||||||
|
const RateLimit = @import("./ratelimit.zig");
|
||||||
|
|
||||||
const FetchError = errors.FetchError;
|
const FetchError = errors.FetchError;
|
||||||
const assert = std.debug.assert;
|
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 SkillUsageResult = @import("./schemas/skill_usage_result.zig");
|
||||||
const EquipResult = @import("./schemas/equip_result.zig");
|
const EquipResult = @import("./schemas/equip_result.zig");
|
||||||
const UnequipResult = EquipResult;
|
const UnequipResult = EquipResult;
|
||||||
pub const GatherResult = SkillUsageResult;
|
const GatherResult = SkillUsageResult;
|
||||||
pub const CraftResult = SkillUsageResult;
|
const CraftResult = SkillUsageResult;
|
||||||
const Image = Store.Image;
|
const Image = Store.Image;
|
||||||
|
|
||||||
const Server = @This();
|
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;
|
const max_response_size = 1024 * 1024 * 16;
|
||||||
|
|
||||||
// TODO: Figure out a way to more accuretly pick a good 'self.fetch_buffer' size
|
// TODO: Figure out a way to more accuretly pick a good 'self.fetch_buffer' size
|
||||||
fetch_buffer: []u8,
|
fetch_buffer: []u8,
|
||||||
client: std.http.Client,
|
client: std.http.Client,
|
||||||
ratelimits: RateLimits,
|
ratelimits: RateLimit.CategoryArray,
|
||||||
|
|
||||||
server_url: ServerURL,
|
server_url: ServerURL,
|
||||||
server_uri: std.Uri,
|
server_uri: std.Uri,
|
||||||
@ -144,7 +55,7 @@ pub fn init(allocator: Allocator, store: *Store) !Server {
|
|||||||
const now = std.time.milliTimestamp();
|
const now = std.time.milliTimestamp();
|
||||||
|
|
||||||
// Limits gotten from https://docs.artifactsmmo.com/api_guide/rate_limits
|
// 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(.account_creation, RateLimit.init(now, 50, null, null));
|
||||||
ratelimits.set(.token, RateLimit.init(now, 50, null, null));
|
ratelimits.set(.token, RateLimit.init(now, 50, null, null));
|
||||||
ratelimits.set(.data, RateLimit.init(now, 7200, 200, 16));
|
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 });
|
log.debug("| payload {s}", .{ payload });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const started_at = std.time.nanoTimestamp();
|
||||||
const result = try self.client.fetch(options);
|
const result = try self.client.fetch(options);
|
||||||
|
const duration_ns = std.time.nanoTimestamp() - started_at;
|
||||||
|
|
||||||
var ratelimit_obj = self.ratelimits.getPtr(ratelimit);
|
var ratelimit_obj = self.ratelimits.getPtr(ratelimit);
|
||||||
ratelimit_obj.increment_counters();
|
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 });
|
log.debug("| status {}", .{ result.status });
|
||||||
|
|
||||||
if (ratelimit != .token) {
|
if (ratelimit != .token) {
|
||||||
|
@ -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 {
|
pub fn get(self: *Images, id: Id) ?*Image {
|
||||||
if (id < self.images.items.len) {
|
if (id < self.images.items.len) {
|
||||||
return &self.images.items[id];
|
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 {
|
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
|
||||||
self.objects.deinit(allocator);
|
self.objects.deinit(allocator);
|
||||||
}
|
}
|
||||||
@ -387,6 +438,44 @@ pub fn deinit(self: *Store, allocator: std.mem.Allocator) void {
|
|||||||
self.ge_orders.deinit(allocator);
|
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 {
|
const SaveData = struct {
|
||||||
api_version: []const u8,
|
api_version: []const u8,
|
||||||
|
|
||||||
|
29
cli/main.zig
29
cli/main.zig
@ -6,6 +6,8 @@ const Allocator = std.mem.Allocator;
|
|||||||
const Artificer = @import("artificer");
|
const Artificer = @import("artificer");
|
||||||
const Api = @import("artifacts-api");
|
const Api = @import("artifacts-api");
|
||||||
|
|
||||||
|
const simulated = true;
|
||||||
|
|
||||||
pub const std_options = .{
|
pub const std_options = .{
|
||||||
.log_scope_levels = &[_]std.log.ScopeLevel{
|
.log_scope_levels = &[_]std.log.ScopeLevel{
|
||||||
.{ .scope = .api, .level = .info },
|
.{ .scope = .api, .level = .info },
|
||||||
@ -58,16 +60,33 @@ pub fn main() !void {
|
|||||||
|
|
||||||
const character_id = (try server.getCharacter("Blondie")).?;
|
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);
|
defer artificer.deinit(allocator);
|
||||||
|
|
||||||
_ = try artificer.appendGoal(Artificer.Goal{
|
_ = try artificer.appendGoal(.{
|
||||||
.equip = .{
|
.gather = .{
|
||||||
.slot = .weapon,
|
.item = store.items.getId("copper_ore").?,
|
||||||
.item = store.items.getId("copper_dagger").?
|
.quantity = 3
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
std.log.info("Starting main loop", .{});
|
std.log.info("Starting main loop", .{});
|
||||||
|
const started_at = artificer.clock.nanoTimestamp();
|
||||||
try artificer.runUntilGoalsComplete();
|
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 });
|
||||||
}
|
}
|
||||||
|
20
gui/app.zig
20
gui/app.zig
@ -21,7 +21,8 @@ const MapTexture = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ui: UI,
|
ui: UI,
|
||||||
artificer: *Artificer,
|
server: *Api.Server,
|
||||||
|
store: *Api.Store,
|
||||||
map_textures: std.ArrayList(MapTexture),
|
map_textures: std.ArrayList(MapTexture),
|
||||||
map_texture_indexes: std.ArrayList(usize),
|
map_texture_indexes: std.ArrayList(usize),
|
||||||
map_position_min: Api.Position,
|
map_position_min: Api.Position,
|
||||||
@ -34,9 +35,7 @@ blur_texture_horizontal: ?rl.RenderTexture = null,
|
|||||||
blur_texture_both: ?rl.RenderTexture = null,
|
blur_texture_both: ?rl.RenderTexture = null,
|
||||||
blur_shader: rl.Shader,
|
blur_shader: rl.Shader,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App {
|
pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server) !App {
|
||||||
const store = artificer.server.store;
|
|
||||||
|
|
||||||
var map_textures = std.ArrayList(MapTexture).init(allocator);
|
var map_textures = std.ArrayList(MapTexture).init(allocator);
|
||||||
errdefer map_textures.deinit();
|
errdefer map_textures.deinit();
|
||||||
errdefer {
|
errdefer {
|
||||||
@ -126,7 +125,8 @@ pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return App{
|
return App{
|
||||||
.artificer = artificer,
|
.store = store,
|
||||||
|
.server = server,
|
||||||
.ui = UI.init(),
|
.ui = UI.init(),
|
||||||
.map_textures = map_textures,
|
.map_textures = map_textures,
|
||||||
.map_texture_indexes = map_texture_indexes,
|
.map_texture_indexes = map_texture_indexes,
|
||||||
@ -598,8 +598,8 @@ pub fn drawWorldAndBlur(self: *App) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn drawRatelimits(self: *App, box: rl.Rectangle) void {
|
fn drawRatelimits(self: *App, box: rl.Rectangle) void {
|
||||||
const Category = Api.Server.RateLimit.Category;
|
const Category = Api.RateLimit.Category;
|
||||||
const ratelimits = self.artificer.server.ratelimits;
|
const ratelimits = self.server.ratelimits;
|
||||||
|
|
||||||
self.drawBlurredWorld(
|
self.drawBlurredWorld(
|
||||||
box,
|
box,
|
||||||
@ -664,10 +664,8 @@ fn drawRatelimits(self: *App, box: rl.Rectangle) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick(self: *App) !void {
|
pub fn tick(self: *App) !void {
|
||||||
var server = self.artificer.server;
|
for (&self.server.ratelimits.values) |*ratelimit| {
|
||||||
try self.artificer.tick();
|
ratelimit.update_timers(std.time.milliTimestamp());
|
||||||
for (&server.ratelimits.values) |*ratelimit| {
|
|
||||||
ratelimit.update_timers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const screen_width = rl.getScreenWidth();
|
const screen_width = rl.getScreenWidth();
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// zig fmt: off
|
// zig fmt: off
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Artificer = @import("artificer");
|
|
||||||
const Api = @import("artifacts-api");
|
const Api = @import("artifacts-api");
|
||||||
const rl = @import("raylib");
|
const rl = @import("raylib");
|
||||||
const raylib_h = @cImport({
|
const raylib_h = @cImport({
|
||||||
@ -11,7 +10,7 @@ const App = @import("./app.zig");
|
|||||||
|
|
||||||
pub const std_options = .{
|
pub const std_options = .{
|
||||||
.log_scope_levels = &[_]std.log.ScopeLevel{
|
.log_scope_levels = &[_]std.log.ScopeLevel{
|
||||||
.{ .scope = .api, .level = .info },
|
.{ .scope = .api, .level = .warn },
|
||||||
.{ .scope = .raylib, .level = .warn },
|
.{ .scope = .raylib, .level = .warn },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -94,9 +93,6 @@ pub fn main() anyerror!void {
|
|||||||
|
|
||||||
try server.setToken(token);
|
try server.setToken(token);
|
||||||
|
|
||||||
var artificer = try Artificer.init(allocator, &server);
|
|
||||||
defer artificer.deinit();
|
|
||||||
|
|
||||||
std.log.info("Prefetching server data", .{});
|
std.log.info("Prefetching server data", .{});
|
||||||
{
|
{
|
||||||
const cwd_path = try std.fs.cwd().realpathAlloc(allocator, ".");
|
const cwd_path = try std.fs.cwd().realpathAlloc(allocator, ".");
|
||||||
@ -117,7 +113,7 @@ pub fn main() anyerror!void {
|
|||||||
.window_resizable = true
|
.window_resizable = true
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = try App.init(allocator, &artificer);
|
var app = try App.init(allocator, &store, &server);
|
||||||
defer app.deinit();
|
defer app.deinit();
|
||||||
|
|
||||||
while (!rl.windowShouldClose()) {
|
while (!rl.windowShouldClose()) {
|
||||||
|
457
lib/artificer.zig
Normal file
457
lib/artificer.zig
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// zig fmt: off
|
// zig fmt: off
|
||||||
const Api = @import("artifacts-api");
|
const Api = @import("artifacts-api");
|
||||||
const Artificer = @import("./root.zig");
|
|
||||||
|
const Artificer = @import("./artificer.zig");
|
||||||
|
const Context = Artificer.GoalContext;
|
||||||
const Requirements = Artificer.Requirements;
|
const Requirements = Artificer.Requirements;
|
||||||
|
|
||||||
const Goal = @This();
|
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 {
|
pub fn tick(self: *Goal, ctx: *Context) !void {
|
||||||
const store = artificer.server.store;
|
const store = ctx.store;
|
||||||
const character = store.characters.get(artificer.character).?;
|
|
||||||
|
|
||||||
if (self.quantity == 0) {
|
if (self.quantity == 0) {
|
||||||
artificer.removeGoal(goal_id);
|
ctx.completed = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,29 +29,27 @@ pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) !void
|
|||||||
const craft = item.craft.?;
|
const craft = item.craft.?;
|
||||||
|
|
||||||
const skill = craft.skill.toCharacterSkill();
|
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;
|
return error.SkillTooLow;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workshop_position = artificer.findNearestWorkstation(craft.skill).?;
|
const workshop_position = ctx.findNearestWorkstation(craft.skill).?;
|
||||||
if (!workshop_position.eql(character.position)) {
|
if (!workshop_position.eql(ctx.character.position)) {
|
||||||
artificer.queued_actions.appendAssumeCapacity(.{
|
ctx.queueAction(.{
|
||||||
.goal = goal_id,
|
.move = workshop_position
|
||||||
.action = .{ .move = workshop_position }
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
artificer.queued_actions.appendAssumeCapacity(.{
|
ctx.queueAction(.{
|
||||||
.goal = goal_id,
|
.craft = .{ .item = self.item, .quantity = self.quantity }
|
||||||
.action = .{ .craft = .{ .item = self.item, .quantity = self.quantity } }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements {
|
pub fn requirements(self: Goal, ctx: *Context) Requirements {
|
||||||
var reqs: Artificer.Requirements = .{};
|
var reqs: Requirements = .{};
|
||||||
|
|
||||||
const store = artificer.server.store;
|
const store = ctx.store;
|
||||||
const item = store.items.get(self.item).?;
|
const item = store.items.get(self.item).?;
|
||||||
const craft = item.craft.?;
|
const craft = item.craft.?;
|
||||||
const craft_multiples = self.getCraftMultiples(craft);
|
const craft_multiples = self.getCraftMultiples(craft);
|
||||||
@ -62,8 +61,8 @@ pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements {
|
|||||||
return reqs;
|
return reqs;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
|
pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void {
|
||||||
_ = goal_id;
|
_ = ctx;
|
||||||
|
|
||||||
if (result == .craft) {
|
if (result == .craft) {
|
||||||
const craft_result = result.craft;
|
const craft_result = result.craft;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// zig fmt: off
|
// zig fmt: off
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Api = @import("artifacts-api");
|
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();
|
const Goal = @This();
|
||||||
|
|
||||||
@ -9,53 +12,45 @@ slot: Api.Character.Equipment.SlotId,
|
|||||||
item: Api.Store.Id,
|
item: Api.Store.Id,
|
||||||
quantity: u64 = 1,
|
quantity: u64 = 1,
|
||||||
|
|
||||||
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) void {
|
pub fn tick(self: *Goal, ctx: *Context) void {
|
||||||
const store = artificer.server.store;
|
const character = ctx.character;
|
||||||
const character = store.characters.get(artificer.character).?;
|
|
||||||
|
|
||||||
const equipment_slot = character.equipment.slots.get(self.slot);
|
const equipment_slot = character.equipment.slots.get(self.slot);
|
||||||
if (equipment_slot.item) |equiped_item|{
|
if (equipment_slot.item) |equiped_item|{
|
||||||
if (equiped_item == self.item and !self.slot.canHoldManyItems()) {
|
if (equiped_item == self.item and !self.slot.canHoldManyItems()) {
|
||||||
artificer.removeGoal(goal_id);
|
ctx.completed = true;
|
||||||
} else {
|
} else {
|
||||||
artificer.queued_actions.appendAssumeCapacity(.{
|
ctx.queueAction(.{
|
||||||
.goal = goal_id,
|
.unequip = .{
|
||||||
.action = .{
|
.slot = self.slot,
|
||||||
.unequip = .{
|
.quantity = self.quantity
|
||||||
.slot = self.slot,
|
|
||||||
.quantity = self.quantity
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
artificer.queued_actions.appendAssumeCapacity(.{
|
ctx.queueAction(.{
|
||||||
.goal = goal_id,
|
.equip = .{
|
||||||
.action = .{
|
.slot = self.slot,
|
||||||
.equip = .{
|
.item = self.item,
|
||||||
.slot = self.slot,
|
.quantity = self.quantity
|
||||||
.item = self.item,
|
|
||||||
.quantity = self.quantity
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements {
|
pub fn requirements(self: Goal, ctx: *Context) Requirements {
|
||||||
_ = artificer;
|
_ = ctx;
|
||||||
|
|
||||||
var reqs: Artificer.Requirements = .{};
|
var reqs: Artificer.Requirements = .{};
|
||||||
reqs.items.addAssumeCapacity(self.item, self.quantity);
|
reqs.items.addAssumeCapacity(self.item, self.quantity);
|
||||||
|
|
||||||
return reqs;
|
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;
|
_ = self;
|
||||||
|
|
||||||
if (result == .equip) {
|
if (result == .equip) {
|
||||||
artificer.removeGoal(goal_id);
|
ctx.completed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,49 @@
|
|||||||
// zig fmt: off
|
// zig fmt: off
|
||||||
const Api = @import("artifacts-api");
|
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();
|
const Goal = @This();
|
||||||
|
|
||||||
item: Api.Store.Id,
|
item: Api.Store.Id,
|
||||||
quantity: u64,
|
quantity: u64,
|
||||||
|
|
||||||
pub fn tick(self: *Goal, goal_id: Artificer.GoalId, artificer: *Artificer) void {
|
pub fn tick(self: *Goal, ctx: *Context) void {
|
||||||
const store = artificer.server.store;
|
|
||||||
const character = store.characters.get(artificer.character).?;
|
|
||||||
|
|
||||||
if (self.quantity == 0) {
|
if (self.quantity == 0) {
|
||||||
artificer.removeGoal(goal_id);
|
ctx.completed = true;
|
||||||
return;
|
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)) {
|
if (!map_position.eql(ctx.character.position)) {
|
||||||
artificer.queued_actions.appendAssumeCapacity(.{
|
ctx.queueAction(.{
|
||||||
.goal = goal_id,
|
.move = map_position
|
||||||
.action = .{ .move = map_position }
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
artificer.queued_actions.appendAssumeCapacity(.{
|
// TODO: Check for enough space in invetory? Or add it as a requirement
|
||||||
.goal = goal_id,
|
ctx.queueAction(.{
|
||||||
.action = .{ .gather = {} }
|
.gather = {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn requirements(self: Goal, artificer: *Artificer) Artificer.Requirements {
|
pub fn requirements(self: Goal, ctx: *Context) Requirements {
|
||||||
|
_ = ctx;
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = artificer;
|
|
||||||
|
|
||||||
const reqs: Artificer.Requirements = .{};
|
const reqs: Requirements = .{};
|
||||||
// TODO: add skill requirement
|
// TODO: add skill requirement
|
||||||
return reqs;
|
return reqs;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onActionCompleted(self: *Goal, goal_id: Artificer.GoalId, result: Artificer.ActionResult) void {
|
pub fn onActionCompleted(self: *Goal, ctx: *Context, result: Artificer.ActionResult) void {
|
||||||
_ = goal_id;
|
_ = ctx;
|
||||||
|
|
||||||
if (result == .gather) {
|
if (result == .gather) {
|
||||||
const gather_result = result.gather;
|
const gather_result = result.gather;
|
||||||
|
421
lib/root.zig
421
lib/root.zig
@ -1,420 +1,11 @@
|
|||||||
// zig fmt: off
|
// zig fmt: off
|
||||||
const std = @import("std");
|
|
||||||
const Api = @import("artifacts-api");
|
const Api = @import("artifacts-api");
|
||||||
const Allocator = std.mem.Allocator;
|
|
||||||
|
|
||||||
const GatherGoal = @import("gather_goal.zig");
|
pub const ArtificerType = @import("./artificer.zig").ArtificerType;
|
||||||
const CraftGoal = @import("craft_goal.zig");
|
pub const SimServer = @import("./sim_server.zig");
|
||||||
const EquipGoal = @import("equip_goal.zig");
|
pub const SimClock = @import("./sim_clock.zig");
|
||||||
|
pub const SystemClock = @import("./system_clock.zig");
|
||||||
|
|
||||||
const assert = std.debug.assert;
|
pub const ArtificerApi = ArtificerType(SystemClock, Api.Server);
|
||||||
const log = std.log.scoped(.artificer);
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
13
lib/sim_clock.zig
Normal file
13
lib/sim_clock.zig
Normal file
@ -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;
|
||||||
|
}
|
227
lib/sim_server.zig
Normal file
227
lib/sim_server.zig
Normal file
@ -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;
|
||||||
|
}
|
12
lib/system_clock.zig
Normal file
12
lib/system_clock.zig
Normal file
@ -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();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user