// zig fmt: off const std = @import("std"); const s2s = @import("s2s"); const Item = @import("./schemas/item.zig"); const SimpleItem = @import("./schemas/simple_item.zig"); const Character = @import("./schemas/character.zig"); const Task = @import("./schemas/task.zig"); const Monster = @import("./schemas/monster.zig"); const Resource = @import("./schemas/resource.zig"); const Map = @import("./schemas/map.zig"); const GEOrder = @import("./schemas/ge_order.zig"); const Position = @import("./schemas/position.zig"); const Skin = Character.Skin; pub const Id = usize; const Store = @This(); const max_character_images = std.meta.fields(Skin).len; const max_character_image_code = size: { var size = Skin.toString(.men1).len; for (std.meta.fields(Skin)) |field| { size = @max(size, Skin.toString(@enumFromInt(field.value)).len); } break :size size; }; pub const Image = struct { pub const Category = enum { character, item, monster, map, resource, effect }; pub const max_code_size = size: { var size: usize = 0; size = @max(size, Item.max_code_size); size = @max(size, max_character_image_code); size = @max(size, Monster.max_code_size); size = @max(size, Resource.max_code_size); size = @max(size, Map.max_skin_size); // TODO: effect code size break :size size; }; pub const Code = std.BoundedArray(u8, max_code_size); code: Code, width: u32, height: u32, rgba_offset: u32, }; const Images = struct { const ImagesArray = std.ArrayListUnmanaged(Image); const IdArray = std.ArrayListUnmanaged(Id); const CategoryMap = std.EnumArray(Image.Category, IdArray); const Options = struct { max_rgba_data: u32, max_items: u32, max_monsters: u32, max_maps: u32, max_resources: u32, max_effects: u32, }; rgba_data: []u8, rgba_fba: std.heap.FixedBufferAllocator, category_mapping: CategoryMap, images: ImagesArray, pub fn initCapacity(allocator: std.mem.Allocator, opts: Options) !Images { const max_characters = std.meta.fields(Character.Skin).len; var max_images: u32 = 0; max_images += @intCast(max_characters); max_images += opts.max_items; max_images += opts.max_monsters; max_images += opts.max_maps; max_images += opts.max_resources; max_images += opts.max_effects; const rgba_data = try allocator.alloc(u8, opts.max_rgba_data); errdefer allocator.free(rgba_data); var images = try ImagesArray.initCapacity(allocator, max_images); errdefer images.deinit(allocator); var character_images = try IdArray.initCapacity(allocator, max_characters); errdefer character_images.deinit(allocator); var item_images = try IdArray.initCapacity(allocator, opts.max_items); errdefer item_images.deinit(allocator); var monster_images = try IdArray.initCapacity(allocator, opts.max_monsters); errdefer monster_images.deinit(allocator); var map_images = try IdArray.initCapacity(allocator, opts.max_maps); errdefer map_images.deinit(allocator); var resource_images = try IdArray.initCapacity(allocator, opts.max_resources); errdefer resource_images.deinit(allocator); var effect_images = try IdArray.initCapacity(allocator, opts.max_effects); 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.init(rgba_data), .category_mapping = category_mapping, .images = images }; } pub fn deinit(self: *Images, allocator: std.mem.Allocator) void { allocator.free(self.rgba_data); self.images.deinit(allocator); self.rgba_fba.reset(); var iter = self.category_mapping.iterator(); while (iter.next()) |id_array| { id_array.value.deinit(allocator); } } 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]; } return null; } pub fn getId(self: *Images, category: Image.Category, code: []const u8) ?Id { const id_array = self.category_mapping.get(category); for (id_array.items) |id| { const image = self.images.items[id]; const image_code = image.code.slice(); if (std.mem.eql(u8, image_code, code)) { return id; } } return null; } pub fn getRGBA(self: *Images, id: Id) ?[]u8 { const image = self.get(id) orelse return null; const rgba_start = image.rgba_offset; const rgba_stop = image.rgba_offset + image.width * image.height * 4; return self.rgba_data[rgba_start..rgba_stop]; } pub fn append(self: *Images, category: Image.Category, code: []const u8, width: u32, height: u32) !Id { if (self.images.unusedCapacitySlice().len == 0) { return error.OutOfMemory; } var image_ids = self.category_mapping.getPtr(category); if (image_ids.unusedCapacitySlice().len == 0) { return error.OutOfMemory; } const rgba_allocator = self.rgba_fba.allocator(); const rgba = try rgba_allocator.alloc(u8, width * height * 4); errdefer rgba_allocator.free(rgba); const image_id = self.images.items.len; self.images.appendAssumeCapacity(Image{ .rgba_offset = @intCast(@intFromPtr(rgba.ptr) - @intFromPtr(self.rgba_data.ptr)), .width = width, .height = height, .code = try Image.Code.fromSlice(code) }); errdefer _ = self.images.pop(); image_ids.appendAssumeCapacity(image_id); return image_id; } }; fn Repository(comptime Object: type, comptime name_field: []const u8) type { const dummy: Object = undefined; const Name: type = @TypeOf(@field(dummy, name_field)); return struct { pub const OptionalObject = union(enum) { reserved: Name, object: Object }; const OptionalObjects = std.ArrayListUnmanaged(OptionalObject); objects: OptionalObjects, pub fn initCapacity(allocator: std.mem.Allocator, capacity: usize) !@This() { return @This(){ .objects = try OptionalObjects.initCapacity(allocator, capacity) }; } 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); } pub fn get(self: *@This(), id: Id) ?*Object { if (id < self.objects.items.len) { const item = &self.objects.items[id]; if (item.* == .object) { return &item.object; } } return null; } pub fn remove(self: *@This(), id: Id) bool { if (id < self.objects.items.len) { const item = &self.objects.items[id]; if (item.* == .object) { item.* = .{ .reserved = @field(item.object, name_field) }; return true; } } return false; } pub fn getId(self: *@This(), name: []const u8) ?Id { for (0.., self.objects.items) |id, object| { const object_name = switch (object) { .object => |obj| @field(obj, name_field).slice(), .reserved => |object_name| object_name.slice(), }; if (std.mem.eql(u8, name, object_name)) { return id; } } return null; } pub fn getName(self: *@This(), id: Id) ?[]const u8 { if (id < self.objects.items.len) { return switch (self.objects.items[id]) { .object => |obj| @field(obj, name_field).slice(), .reserved => |name| name.slice(), }; } return null; } pub fn getOrReserveId(self: *@This(), name: []const u8) !Id { if (self.getId(name)) |id| { return id; } if (self.objects.unusedCapacitySlice().len == 0) { return error.OutOfMemory; } const owned_name = try Name.fromSlice(name); self.objects.appendAssumeCapacity(.{ .reserved = owned_name }); return self.objects.items.len - 1; } pub fn appendOrUpdate(self: *@This(), item: Object) !Id { if (self.getId(@field(item, name_field).slice())) |id| { self.objects.items[id] = .{ .object = item }; return id; } else { if (self.objects.unusedCapacitySlice().len == 0) { return error.OutOfMemory; } self.objects.appendAssumeCapacity(.{ .object = item }); return self.objects.items.len - 1; } } }; } const Items = Repository(Item, "code"); const Characters = Repository(Character, "name"); const Tasks = Repository(Task, "code"); const Monsters = Repository(Monster, "code"); const Resources = Repository(Resource, "code"); const GEOrders = Repository(GEOrder, "id"); const Maps = std.ArrayListUnmanaged(Map); const Bank = SimpleItem.BoundedArray(64); items: Items, characters: Characters, tasks: Tasks, monsters: Monsters, resources: Resources, images: Images, maps: Maps, bank: Bank, ge_orders: GEOrders, pub fn init(allocator: std.mem.Allocator) !Store { const max_items = 512; const max_characters = 10; const max_tasks = 32; const max_monsters = 64; const max_resources = 32; const max_maps = 512; const max_ge_orders = 128; var items = try Items.initCapacity(allocator, max_items); errdefer items.deinit(allocator); var characters = try Characters.initCapacity(allocator, max_characters); errdefer characters.deinit(allocator); var tasks = try Tasks.initCapacity(allocator, max_tasks); errdefer tasks.deinit(allocator); var monsters = try Monsters.initCapacity(allocator, max_monsters); errdefer monsters.deinit(allocator); var resources = try Resources.initCapacity(allocator, max_resources); errdefer resources.deinit(allocator); var images = try Images.initCapacity(allocator, .{ .max_rgba_data = 1024 * 1024 * 32, .max_items = max_items, .max_effects = 16, .max_resources = max_resources, .max_maps = max_maps, .max_monsters = max_monsters, }); errdefer images.deinit(allocator); var maps = try Maps.initCapacity(allocator, max_maps); errdefer maps.deinit(allocator); var ge_orders = try GEOrders.initCapacity(allocator, max_ge_orders); errdefer ge_orders.deinit(allocator); const bank = Bank.init(); return Store{ .items = items, .characters = characters, .tasks = tasks, .monsters = monsters, .resources = resources, .maps = maps, .images = images, .bank = bank, .ge_orders = ge_orders }; } pub fn deinit(self: *Store, allocator: std.mem.Allocator) void { self.items.deinit(allocator); self.characters.deinit(allocator); self.tasks.deinit(allocator); self.monsters.deinit(allocator); self.resources.deinit(allocator); self.maps.deinit(allocator); self.images.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 { api_version: []const u8, items: []Items.OptionalObject, characters: []Characters.OptionalObject, tasks: []Tasks.OptionalObject, monsters: []Monsters.OptionalObject, resources: []Resources.OptionalObject, maps: []Map, images: struct { rgba_data: []u8, images: []Image, // These next fields are just `category_mapping` character_mapping: []Id, item_mapping: []Id, monster_mapping: []Id, map_mapping: []Id, resource_mapping: []Id, effect_mapping: []Id, } }; // TODO: Create a better serialized that doesn't write zeros or BoundedArray types pub fn save(self: *Store, api_version: []const u8, writer: anytype) !void { const rgba_end_index = self.images.rgba_fba.end_index; const data = SaveData{ .api_version = api_version, .items = self.items.objects.items, .characters = self.characters.objects.items, .tasks = self.tasks.objects.items, .resources = self.resources.objects.items, .monsters = self.monsters.objects.items, .maps = self.maps.items, .images = .{ .rgba_data = self.images.rgba_data[0..rgba_end_index], .images = self.images.images.items, .character_mapping = self.images.category_mapping.get(.character).items, .item_mapping = self.images.category_mapping.get(.item).items, .monster_mapping = self.images.category_mapping.get(.monster).items, .map_mapping = self.images.category_mapping.get(.map).items, .resource_mapping = self.images.category_mapping.get(.resource).items, .effect_mapping = self.images.category_mapping.get(.effect).items, } }; try s2s.serialize(writer, SaveData, data); } pub fn load(self: *Store, allocator: std.mem.Allocator, api_version: []const u8, reader: anytype) !void { var data: SaveData = try s2s.deserializeAlloc(reader, SaveData, allocator); defer s2s.free(allocator, SaveData, &data); // TODO: Add better version checking. // * Hash the layout of `SaveData` and save that hash into the file. To see if layout changed. // * Hash the openapi documentation file to see if the saved data is out-of-date and should be ignored. if (!std.mem.eql(u8, data.api_version, api_version)) { return error.InvalidVersion; } const repositories = .{ .{ &self.items, data.items }, .{ &self.characters, data.characters }, .{ &self.tasks, data.tasks }, .{ &self.resources, data.resources }, .{ &self.monsters, data.monsters }, }; const image_category_mapping = &self.images.category_mapping; const image_categories = .{ .{ image_category_mapping.getPtr(.character), data.images.character_mapping }, .{ image_category_mapping.getPtr(.item), data.images.item_mapping }, .{ image_category_mapping.getPtr(.monster), data.images.monster_mapping }, .{ image_category_mapping.getPtr(.map), data.images.map_mapping }, .{ image_category_mapping.getPtr(.resource), data.images.resource_mapping }, .{ image_category_mapping.getPtr(.effect), data.images.effect_mapping }, }; // Check if there is enough space { inline for (repositories) |pair| { const repository = pair[0]; const saved_objects = pair[1]; if (saved_objects.len > repository.objects.capacity) { return error.OutOfMemory; } } if (data.maps.len > self.maps.capacity) { return error.OutOfMemory; } if (data.images.rgba_data.len > self.images.rgba_data.len) { return error.OutOfMemory; } if (data.images.images.len > self.images.images.capacity) { return error.OutOfMemory; } inline for (image_categories) |pair| { const self_ids = pair[0]; const saved_ids = pair[1]; if (saved_ids.len > self_ids.capacity) { return error.OutOfMemory; } } } // Move loaded data from file to this store { inline for (repositories) |pair| { const repository = pair[0]; const saved_objects = pair[1]; repository.objects.clearRetainingCapacity(); repository.objects.appendSliceAssumeCapacity(saved_objects); } self.maps.clearRetainingCapacity(); self.maps.appendSliceAssumeCapacity(data.maps); @memcpy(self.images.rgba_data[0..data.images.rgba_data.len], data.images.rgba_data); self.images.rgba_fba.end_index = data.images.rgba_data.len; self.images.images.clearRetainingCapacity(); self.images.images.appendSliceAssumeCapacity(data.images.images); inline for (image_categories) |pair| { const self_ids = pair[0]; const saved_ids = pair[1]; self_ids.clearRetainingCapacity(); self_ids.appendSliceAssumeCapacity(saved_ids); } } } pub fn appendOrUpdateMap(self: *Store, map: Map) !Position { if (self.getMap(map.position)) |existing_map| { existing_map.* = map; } else { if (self.maps.unusedCapacitySlice().len == 0) { return error.OutOfMemory; } self.maps.appendAssumeCapacity(map); } return map.position; } pub fn getMap(self: *Store, position: Position) ?*Map { for (self.maps.items) |*map| { if (map.position.eql(position)) { return map; } } return null; }