diff --git a/api/root.zig b/api/root.zig index c9bb65f..5b9b311 100644 --- a/api/root.zig +++ b/api/root.zig @@ -4,6 +4,7 @@ pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime; pub const Server = @import("server.zig"); pub const Store = @import("store.zig"); pub const Character = @import("./schemas/character.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; diff --git a/api/schemas/drop_rate.zig b/api/schemas/drop_rate.zig index c2179ab..46c92b5 100644 --- a/api/schemas/drop_rate.zig +++ b/api/schemas/drop_rate.zig @@ -27,7 +27,7 @@ pub fn parse(store: *Store, obj: json.ObjectMap) !DropRate { } const code_str = try json_utils.getStringRequired(obj, "code"); - const item_id = try store.getItemId(code_str); + const item_id = try store.getCodeId(code_str); return DropRate{ .item_id = item_id, diff --git a/api/schemas/item.zig b/api/schemas/item.zig index 66512d5..60bd116 100644 --- a/api/schemas/item.zig +++ b/api/schemas/item.zig @@ -38,9 +38,8 @@ pub const TypeUtils = EnumStringUtils(Type, .{ .{ "currency" , .currency }, }); -allocator: Allocator, name: []u8, -code: []u8, +code_id: Store.CodeId, level: u64, type: Type, subtype: []u8, @@ -56,9 +55,8 @@ pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Item { const item_type_str = try json_utils.getStringRequired(obj, "type"); return Item{ - .allocator = allocator, .name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty, - .code = (try json_utils.dupeString(allocator, obj, "code")) orelse return error.MissingProperty, + .code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty, .level = @intCast(level), .type = TypeUtils.fromString(item_type_str) orelse return error.InvalidType, .subtype = (try json_utils.dupeString(allocator, obj, "subtype")) orelse return error.MissingProperty, @@ -67,9 +65,8 @@ pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Item { }; } -pub fn deinit(self: Item) void { - self.allocator.free(self.name); - self.allocator.free(self.code); - self.allocator.free(self.subtype); - self.allocator.free(self.description); +pub fn deinit(self: Item, allocator: Allocator) void { + allocator.free(self.name); + allocator.free(self.subtype); + allocator.free(self.description); } diff --git a/api/schemas/monster.zig b/api/schemas/monster.zig index 68488c0..e74d37e 100644 --- a/api/schemas/monster.zig +++ b/api/schemas/monster.zig @@ -59,10 +59,10 @@ pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Monster } return Monster{ - .name = try json_utils.dupeStringRequired(allocator, obj, "name"), - .code = (try store.getCodeIdJson(allocator, obj, "code")) orelse return error.MissingProperty, - .level = @intCast(level), - .hp = @intCast(hp), + .name = try json_utils.dupeStringRequired(allocator, obj, "name"), + .code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty, + .level = @intCast(level), + .hp = @intCast(hp), .fire = try ElementalStats.parse(obj, "attack_fire" , "res_fire" ), .earth = try ElementalStats.parse(obj, "attack_earth", "res_earth"), diff --git a/api/schemas/resource.zig b/api/schemas/resource.zig index c12569e..4fd4f3a 100644 --- a/api/schemas/resource.zig +++ b/api/schemas/resource.zig @@ -38,11 +38,11 @@ pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Resource const skill_str = try json_utils.getStringRequired(obj, "skill"); return Resource{ - .name = try json_utils.dupeStringRequired(allocator, obj, "name"), - .code = (try store.getCodeIdJson(allocator, obj, "code")) orelse return error.MissingProperty, - .level = @intCast(level), - .skill = SkillUtils.fromString(skill_str) orelse return error.InvalidSkill, - .drops = try DropRate.parseList(store, drops_array) + .name = try json_utils.dupeStringRequired(allocator, obj, "name"), + .code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty, + .level = @intCast(level), + .skill = SkillUtils.fromString(skill_str) orelse return error.InvalidSkill, + .drops = try DropRate.parseList(store, drops_array) }; } diff --git a/api/server.zig b/api/server.zig index 247978d..c421fbe 100644 --- a/api/server.zig +++ b/api/server.zig @@ -407,6 +407,26 @@ pub fn prefetch(self: *Server) !void { self.prefetched = true; } +pub fn prefetchCached(self: *Server, cache_path: []const u8) !void { + var status: ServerStatus = try self.getServerStatus(); + defer status.deinit(); + + if (std.fs.openFileAbsolute(cache_path, .{})) |file| { + defer file.close(); + + if (self.store.load(status.version, file.reader())) { + self.prefetched = true; + return; + } else |_| { } + } else |_| { } + + try self.prefetch(); + + const file = try std.fs.createFileAbsolute(cache_path, .{}); + defer file.close(); + try self.store.save(status.version, file.writer()); +} + // ------------------------- Endpoints ------------------------ pub fn getServerStatus(self: *Server) FetchError!ServerStatus { diff --git a/api/store.zig b/api/store.zig index e20ada5..4a46330 100644 --- a/api/store.zig +++ b/api/store.zig @@ -1,6 +1,7 @@ const std = @import("std"); const json_utils = @import("json_utils.zig"); const Server = @import("./server.zig"); +const s2s = @import("s2s"); const json = std.json; const Allocator = std.mem.Allocator; const assert = std.debug.assert; @@ -17,61 +18,48 @@ const DropRate = @import("./schemas/drop_rate.zig"); pub const CodeId = u16; +const Characters = std.ArrayList(Character); +const ItemsMap = std.StringHashMap(Item); +const MapsMap = std.AutoHashMap(Position, Map); +const ResourcesMap = std.StringHashMap(Resource); +const MonstersMap = std.StringHashMap(Monster); + allocator: Allocator, codes: std.ArrayList([]u8), -characters: std.ArrayList(Character), -items: std.StringHashMap(Item), -maps: std.AutoHashMap(Position, Map), -resources: std.StringHashMap(Resource), -monsters: std.StringHashMap(Monster), +characters: Characters, +items: ItemsMap, +maps: MapsMap, +resources: ResourcesMap, +monsters: MonstersMap, // TODO: bank pub fn init(allocator: Allocator) Store { return Store{ .allocator = allocator, .codes = std.ArrayList([]u8).init(allocator), - .characters = std.ArrayList(Character).init(allocator), - .items = std.StringHashMap(Item).init(allocator), - .maps = std.AutoHashMap(Position, Map).init(allocator), - .resources = std.StringHashMap(Resource).init(allocator), - .monsters = std.StringHashMap(Monster).init(allocator), + .characters = Characters.init(allocator), + .items = ItemsMap.init(allocator), + .maps = MapsMap.init(allocator), + .resources = ResourcesMap.init(allocator), + .monsters = MonstersMap.init(allocator), }; } pub fn deinit(self: *Store) void { - for (self.codes.items) |code| { - self.allocator.free(code); - } - self.codes.deinit(); + self.clearCodes(); for (self.characters.items) |*char| { char.deinit(); } self.characters.deinit(); - var itemsIter = self.items.valueIterator(); - while (itemsIter.next()) |item| { - item.deinit(); - } - self.items.deinit(); + self.clearItems(); - var mapsIter = self.maps.valueIterator(); - while (mapsIter.next()) |map| { - map.deinit(self.allocator); - } - self.maps.deinit(); + self.clearMaps(); - var resourcesIter = self.resources.valueIterator(); - while (resourcesIter.next()) |resource| { - resource.deinit(self.allocator); - } - self.resources.deinit(); + self.clearResources(); - var monstersIter = self.monsters.valueIterator(); - while (monstersIter.next()) |monster| { - monster.deinit(self.allocator); - } - self.monsters.deinit(); + self.clearMonsters(); } pub fn getCodeId(self: *Store, code: []const u8) !CodeId { @@ -107,6 +95,101 @@ pub fn getCodeIdJson(self: *Store, object: json.ObjectMap, name: []const u8) !?C return try self.getCodeId(code); } +fn clearCodes(self: *Store) void { + for (self.codes.items) |code| { + self.allocator.free(code); + } + self.codes.clearAndFree(); +} + +// ----------------------- Storing to file ------------------------------ + +const SaveData = struct { + api_version: []const u8, + + codes: [][]u8, + items: []Item, + maps: []Map, + resources: []Resource, + monsters: []Monster, +}; + +fn allocHashMapValues(Value: type, allocator: Allocator, hashmap: anytype) ![]Value { + var values = try allocator.alloc(Value, hashmap.count()); + errdefer allocator.free(values); + + var valueIter = hashmap.valueIterator(); + var index: usize = 0; + while (valueIter.next()) |value| { + values[index] = value.*; + index += 1; + } + + return values; +} + +pub fn save(self: *Store, api_version: []const u8, writer: anytype) !void { + const items = try allocHashMapValues(Item, self.allocator, self.items); + defer self.allocator.free(items); + + const maps = try allocHashMapValues(Map, self.allocator, self.maps); + defer self.allocator.free(maps); + + const resources = try allocHashMapValues(Resource, self.allocator, self.resources); + defer self.allocator.free(resources); + + const monsters = try allocHashMapValues(Monster, self.allocator, self.monsters); + defer self.allocator.free(monsters); + + const data = SaveData{ + .api_version = api_version, + .codes = self.codes.items, + .items = items, + .maps = maps, + .resources = resources, + .monsters = monsters + }; + + try s2s.serialize(writer, SaveData, data); +} + +pub fn load(self: *Store, api_version: []const u8, reader: anytype) !void { + var data = try s2s.deserializeAlloc(reader, SaveData, self.allocator); + if (!std.mem.eql(u8, data.api_version, api_version)) { + s2s.free(self.allocator, SaveData, &data); + return error.InvalidVersion; + } + defer self.allocator.free(data.api_version); + + self.clearCodes(); + try self.codes.appendSlice(data.codes); + defer self.allocator.free(data.codes); + + self.clearItems(); + for (data.items) |item| { + try self.putItem(item); + } + defer self.allocator.free(data.items); + + self.clearMaps(); + for (data.maps) |map| { + try self.putMap(map); + } + defer self.allocator.free(data.maps); + + self.clearResources(); + for (data.resources) |resource| { + try self.putResource(resource); + } + defer self.allocator.free(data.resources); + + self.clearMonsters(); + for (data.monsters) |monster| { + try self.putMonster(monster); + } + defer self.allocator.free(data.monsters); +} + // ----------------------- Character ------------------------------ fn getCharacterIndex(self: *const Store, name: []const u8) ?usize { @@ -174,6 +257,14 @@ pub fn putMap(self: *Store, map: Map) !void { entry.value_ptr.* = map; } +fn clearMaps(self: *Store) void { + var mapsIter = self.maps.valueIterator(); + while (mapsIter.next()) |map| { + map.deinit(self.allocator); + } + self.maps.clearAndFree(); +} + // ----------------------- Item ------------------------------ pub fn getItem(self: *Store, code: []const u8) ?Item { @@ -194,7 +285,7 @@ pub fn getItems(self: *Store, opts: Server.ItemOptions) !std.ArrayList(Item) { if (item.craft == null) continue; const recipe = item.craft.?; - const craft_material_id = try self.getItemId(craft_material); + const craft_material_id = try self.getCodeId(craft_material); const material_quantity = recipe.items.getQuantity(craft_material_id); if (material_quantity == 0) continue; } @@ -218,13 +309,22 @@ pub fn getItems(self: *Store, opts: Server.ItemOptions) !std.ArrayList(Item) { } pub fn putItem(self: *Store, item: Item) !void { - var entry = try self.items.getOrPut(item.code); + const code = self.getCode(item.code_id).?; + var entry = try self.items.getOrPut(code); if (entry.found_existing) { - entry.value_ptr.deinit(); + entry.value_ptr.deinit(self.allocator); } entry.value_ptr.* = item; } +fn clearItems(self: *Store) void { + var itemsIter = self.items.valueIterator(); + while (itemsIter.next()) |item| { + item.deinit(self.allocator); + } + self.items.clearAndFree(); +} + // ----------------------- Monster ------------------------------ pub fn getMonster(self: *Store, code: []const u8) ?Monster { @@ -244,7 +344,7 @@ pub fn getMonsters(self: *Store, opts: Server.MonsterOptions) !std.ArrayList(Mon if (monster.level > max_level) continue; } if (opts.drop) |drop| { - const item_id = try self.getItemId(drop); + const item_id = try self.getCodeId(drop); if (!DropRate.doesListContain(&monster.drops, item_id)) { continue; } @@ -257,13 +357,22 @@ pub fn getMonsters(self: *Store, opts: Server.MonsterOptions) !std.ArrayList(Mon } pub fn putMonster(self: *Store, monster: Monster) !void { - var entry = try self.resources.getOrPut(monster.code_id); + const code = self.getCode(monster.code_id).?; + var entry = try self.monsters.getOrPut(code); if (entry.found_existing) { entry.value_ptr.deinit(self.allocator); } entry.value_ptr.* = monster; } +fn clearMonsters(self: *Store) void { + var monstersIter = self.monsters.valueIterator(); + while (monstersIter.next()) |monster| { + monster.deinit(self.allocator); + } + self.monsters.clearAndFree(); +} + // ----------------------- Resource ------------------------------ pub fn getResource(self: *Store, code: []const u8) ?Resource { @@ -283,7 +392,7 @@ pub fn getResources(self: *Store, opts: Server.ResourceOptions) !std.ArrayList(R if (resource.level > max_level) continue; } if (opts.drop) |drop| { - const item_id = try self.getItemId(drop); + const item_id = try self.getCodeId(drop); if (!DropRate.doesListContain(&resource.drops, item_id)) { continue; } @@ -296,9 +405,18 @@ pub fn getResources(self: *Store, opts: Server.ResourceOptions) !std.ArrayList(R } pub fn putResource(self: *Store, resource: Resource) !void { - var entry = try self.resources.getOrPut(resource.code); + const code = self.getCode(resource.code_id).?; + var entry = try self.resources.getOrPut(code); if (entry.found_existing) { entry.value_ptr.deinit(self.allocator); } entry.value_ptr.* = resource; } + +fn clearResources(self: *Store) void { + var resourcesIter = self.resources.valueIterator(); + while (resourcesIter.next()) |resource| { + resource.deinit(self.allocator); + } + self.resources.clearAndFree(); +} diff --git a/build.zig b/build.zig index ceaa814..ef65365 100644 --- a/build.zig +++ b/build.zig @@ -9,6 +9,11 @@ pub fn build(b: *std.Build) void { var api: *Module = undefined; { + const s2s_dep = b.dependency("s2s", .{ + .target = target, + .optimize = optimize, + }); + api = b.createModule(.{ .root_source_file = b.path("api/root.zig"), .target = target, @@ -17,6 +22,7 @@ pub fn build(b: *std.Build) void { }); api.addIncludePath(b.path("api/date_time")); api.addCSourceFile(.{ .file = b.path("api/date_time/timegm.c") }); + api.addImport("s2s", s2s_dep.module("s2s")); } var lib: *Module = undefined; @@ -47,6 +53,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); cli.root_module.addImport("artificer", lib); + cli.root_module.addImport("artifacts-api", api); b.installArtifact(cli); const run_cmd = b.addRunArtifact(cli); diff --git a/build.zig.zon b/build.zig.zon index 4338e9e..2ec7198 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -3,10 +3,14 @@ .version = "0.1.0", .minimum_zig_version = "0.12.0", .dependencies = .{ - .@"raylib-zig" = .{ - .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", - .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" - }, + .@"raylib-zig" = .{ + .url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz", + .hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212" + }, + .s2s = .{ + .url = "https://github.com/ziglibs/s2s/archive/b30205d5e9204899fb6d0fdf28d00ed4d18fe9c9.tar.gz", + .hash = "12202c39c98f05041f1052c268132669dbfcda87e4dbb0353cd84a6070924c8ac0e3", + }, }, - .paths = .{ "" }, + .paths = .{""}, } diff --git a/cli/main.zig b/cli/main.zig index e363579..5618b8f 100644 --- a/cli/main.zig +++ b/cli/main.zig @@ -1,8 +1,10 @@ const std = @import("std"); const builtin = @import("builtin"); -const Artificer = @import("artificer"); const Allocator = std.mem.Allocator; +const Artificer = @import("artificer"); +const Api = @import("artifacts-api"); + fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 { const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); @@ -30,11 +32,13 @@ pub fn main() !void { var artificer = try Artificer.init(allocator, token); defer artificer.deinit(); - if (builtin.mode != .Debug) { - std.log.info("Prefetching server data", .{}); - try artificer.server.prefetch(); - } + const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin"); + defer allocator.free(cache_path); + std.log.info("Prefetching server data", .{}); + try artificer.server.prefetchCached(cache_path); + + if (false) { std.log.info("Starting main loop", .{}); while (true) { const waitUntil = artificer.nextStepAt(); @@ -45,4 +49,5 @@ pub fn main() !void { try artificer.step(); } + } } diff --git a/gui/main.zig b/gui/main.zig index 5d7e2cf..8065c25 100644 --- a/gui/main.zig +++ b/gui/main.zig @@ -66,6 +66,12 @@ pub fn main() anyerror!void { var artificer = try Artificer.init(allocator, token); defer artificer.deinit(); + const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin"); + defer allocator.free(cache_path); + + std.log.info("Prefetching server data", .{}); + try artificer.server.prefetchCached(cache_path); + rl.initWindow(800, 450, "Artificer"); defer rl.closeWindow();