643 lines
20 KiB
Zig
643 lines
20 KiB
Zig
// 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;
|
|
}
|