artificer/lib/api/store.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;
}