mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-09-08 19:50:23 -04:00
Blueprint asset loading (#1207)
* SBB * Fix rotation code * Remove rotate command * Fix segfault during generation * Delete format.bat * Pregenerate all rotated blueprints * Apply review suggestions * Apply review suggestions * Change child block storage model * Resolve child structures during load * Move structure_building_blocks.zig to terrain * Update src/rotation.zig * Apply review suggestions * Remove sbb blocks * Remove SBBGen * Remove example SBB * Remove blueprint code * Remove createAssetStringID * Apply review suggestions * Apply review suggestions * Add asset ID generation * Revert "Add asset ID generation" This reverts commit ffe8fd281486f2124ab83b0e614b6a7db97a020d. * Remove SBB loading * Apply review requests
This commit is contained in:
parent
0e9ffda1b1
commit
1ec150d624
@ -3,9 +3,12 @@ const std = @import("std");
|
|||||||
const blocks_zig = @import("blocks.zig");
|
const blocks_zig = @import("blocks.zig");
|
||||||
const items_zig = @import("items.zig");
|
const items_zig = @import("items.zig");
|
||||||
const migrations_zig = @import("migrations.zig");
|
const migrations_zig = @import("migrations.zig");
|
||||||
|
const blueprints_zig = @import("blueprint.zig");
|
||||||
|
const Blueprint = blueprints_zig.Blueprint;
|
||||||
const ZonElement = @import("zon.zig").ZonElement;
|
const ZonElement = @import("zon.zig").ZonElement;
|
||||||
const main = @import("main.zig");
|
const main = @import("main.zig");
|
||||||
const biomes_zig = main.server.terrain.biomes;
|
const biomes_zig = main.server.terrain.biomes;
|
||||||
|
const sbb = main.server.terrain.structure_building_blocks;
|
||||||
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
|
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
|
||||||
|
|
||||||
var arena: main.heap.NeverFailingArenaAllocator = undefined;
|
var arena: main.heap.NeverFailingArenaAllocator = undefined;
|
||||||
@ -18,6 +21,7 @@ var commonBiomes: std.StringHashMap(ZonElement) = undefined;
|
|||||||
var commonBiomeMigrations: std.StringHashMap(ZonElement) = undefined;
|
var commonBiomeMigrations: std.StringHashMap(ZonElement) = undefined;
|
||||||
var commonRecipes: std.StringHashMap(ZonElement) = undefined;
|
var commonRecipes: std.StringHashMap(ZonElement) = undefined;
|
||||||
var commonModels: std.StringHashMap([]const u8) = undefined;
|
var commonModels: std.StringHashMap([]const u8) = undefined;
|
||||||
|
var commonBlueprints: std.StringHashMap([]u8) = undefined;
|
||||||
|
|
||||||
pub fn init() void {
|
pub fn init() void {
|
||||||
biomes_zig.init();
|
biomes_zig.init();
|
||||||
@ -33,6 +37,7 @@ pub fn init() void {
|
|||||||
commonBiomeMigrations = .init(arenaAllocator.allocator);
|
commonBiomeMigrations = .init(arenaAllocator.allocator);
|
||||||
commonRecipes = .init(arenaAllocator.allocator);
|
commonRecipes = .init(arenaAllocator.allocator);
|
||||||
commonModels = .init(arenaAllocator.allocator);
|
commonModels = .init(arenaAllocator.allocator);
|
||||||
|
commonBlueprints = .init(arenaAllocator.allocator);
|
||||||
|
|
||||||
readAssets(
|
readAssets(
|
||||||
arenaAllocator,
|
arenaAllocator,
|
||||||
@ -45,11 +50,12 @@ pub fn init() void {
|
|||||||
&commonBiomeMigrations,
|
&commonBiomeMigrations,
|
||||||
&commonRecipes,
|
&commonRecipes,
|
||||||
&commonModels,
|
&commonModels,
|
||||||
|
&commonBlueprints,
|
||||||
);
|
);
|
||||||
|
|
||||||
std.log.info(
|
std.log.info(
|
||||||
"Finished assets init with {} blocks ({} migrations), {} items, {} tools. {} biomes ({} migrations), {} recipes",
|
"Finished assets init with {} blocks ({} migrations), {} items, {} tools, {} biomes ({} migrations), {} recipes, and {} blueprints",
|
||||||
.{commonBlocks.count(), commonBlockMigrations.count(), commonItems.count(), commonTools.count(), commonBiomes.count(), commonBiomeMigrations.count(), commonRecipes.count()},
|
.{commonBlocks.count(), commonBlockMigrations.count(), commonItems.count(), commonTools.count(), commonBiomes.count(), commonBiomeMigrations.count(), commonRecipes.count(), commonBlueprints.count()},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +155,7 @@ pub fn readAllZonFilesInAddons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn createAssetStringID(
|
fn createAssetStringID(
|
||||||
externalAllocator: NeverFailingAllocator,
|
externalAllocator: NeverFailingAllocator,
|
||||||
addonName: []const u8,
|
addonName: []const u8,
|
||||||
@ -173,6 +180,45 @@ fn createAssetStringID(
|
|||||||
|
|
||||||
return assetId;
|
return assetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn readAllBlueprintFilesInAddons(
|
||||||
|
externalAllocator: NeverFailingAllocator,
|
||||||
|
addons: main.List(std.fs.Dir),
|
||||||
|
addonNames: main.List([]const u8),
|
||||||
|
subPath: []const u8,
|
||||||
|
output: *std.StringHashMap([]u8),
|
||||||
|
) void {
|
||||||
|
for(addons.items, addonNames.items) |addon, addonName| {
|
||||||
|
var dir = addon.openDir(subPath, .{.iterate = true}) catch |err| {
|
||||||
|
if(err != error.FileNotFound) {
|
||||||
|
std.log.err("Could not open addon directory {s}: {s}", .{subPath, @errorName(err)});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
defer dir.close();
|
||||||
|
|
||||||
|
var walker = dir.walk(main.stackAllocator.allocator) catch unreachable;
|
||||||
|
defer walker.deinit();
|
||||||
|
|
||||||
|
while(walker.next() catch |err| blk: {
|
||||||
|
std.log.err("Got error while iterating addon directory {s}: {s}", .{subPath, @errorName(err)});
|
||||||
|
break :blk null;
|
||||||
|
}) |entry| {
|
||||||
|
if(entry.kind != .file) continue;
|
||||||
|
if(std.ascii.startsWithIgnoreCase(entry.basename, "_defaults")) continue;
|
||||||
|
if(!std.ascii.endsWithIgnoreCase(entry.basename, ".blp")) continue;
|
||||||
|
if(std.ascii.startsWithIgnoreCase(entry.basename, "_migrations")) continue;
|
||||||
|
|
||||||
|
const stringId: []u8 = createAssetStringID(externalAllocator, addonName, entry.path);
|
||||||
|
const data = main.files.Dir.init(dir).read(externalAllocator, entry.path) catch |err| {
|
||||||
|
std.log.err("Could not open {s}/{s}: {s}", .{subPath, entry.path, @errorName(err)});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
output.put(stringId, data) catch unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Reads obj files recursively from all subfolders.
|
/// Reads obj files recursively from all subfolders.
|
||||||
pub fn readAllObjFilesInAddonsHashmap(
|
pub fn readAllObjFilesInAddonsHashmap(
|
||||||
externalAllocator: NeverFailingAllocator,
|
externalAllocator: NeverFailingAllocator,
|
||||||
@ -221,6 +267,7 @@ pub fn readAssets(
|
|||||||
biomeMigrations: *std.StringHashMap(ZonElement),
|
biomeMigrations: *std.StringHashMap(ZonElement),
|
||||||
recipes: *std.StringHashMap(ZonElement),
|
recipes: *std.StringHashMap(ZonElement),
|
||||||
models: *std.StringHashMap([]const u8),
|
models: *std.StringHashMap([]const u8),
|
||||||
|
blueprints: *std.StringHashMap([]u8),
|
||||||
) void {
|
) void {
|
||||||
var addons = main.List(std.fs.Dir).init(main.stackAllocator);
|
var addons = main.List(std.fs.Dir).init(main.stackAllocator);
|
||||||
defer addons.deinit();
|
defer addons.deinit();
|
||||||
@ -258,6 +305,7 @@ pub fn readAssets(
|
|||||||
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "biomes", true, biomes, biomeMigrations);
|
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "biomes", true, biomes, biomeMigrations);
|
||||||
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "recipes", false, recipes, null);
|
readAllZonFilesInAddons(externalAllocator, addons, addonNames, "recipes", false, recipes, null);
|
||||||
readAllObjFilesInAddonsHashmap(externalAllocator, addons, addonNames, "models", models);
|
readAllObjFilesInAddonsHashmap(externalAllocator, addons, addonNames, "models", models);
|
||||||
|
readAllBlueprintFilesInAddons(externalAllocator, addons, addonNames, "blueprints", blueprints);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void {
|
fn registerItem(assetFolder: []const u8, id: []const u8, zon: ZonElement) !void {
|
||||||
@ -424,6 +472,8 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
|
|||||||
defer recipes.clearAndFree();
|
defer recipes.clearAndFree();
|
||||||
var models = commonModels.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
|
var models = commonModels.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
|
||||||
defer models.clearAndFree();
|
defer models.clearAndFree();
|
||||||
|
var blueprints = commonBlueprints.cloneWithAllocator(main.stackAllocator.allocator) catch unreachable;
|
||||||
|
defer blueprints.clearAndFree();
|
||||||
|
|
||||||
readAssets(
|
readAssets(
|
||||||
arenaAllocator,
|
arenaAllocator,
|
||||||
@ -436,6 +486,7 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
|
|||||||
&biomeMigrations,
|
&biomeMigrations,
|
||||||
&recipes,
|
&recipes,
|
||||||
&models,
|
&models,
|
||||||
|
&blueprints,
|
||||||
);
|
);
|
||||||
errdefer unloadAssets();
|
errdefer unloadAssets();
|
||||||
|
|
||||||
@ -542,6 +593,8 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
|
|||||||
registerRecipesFromZon(entry.value_ptr.*);
|
registerRecipesFromZon(entry.value_ptr.*);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try sbb.registerBlueprints(&blueprints);
|
||||||
|
|
||||||
// Biomes:
|
// Biomes:
|
||||||
var nextBiomeNumericId: u32 = 0;
|
var nextBiomeNumericId: u32 = 0;
|
||||||
for(biomePalette.palette.items) |id| {
|
for(biomePalette.palette.items) |id| {
|
||||||
@ -577,7 +630,7 @@ pub fn loadWorldAssets(assetFolder: []const u8, blockPalette: *Palette, itemPale
|
|||||||
}
|
}
|
||||||
|
|
||||||
std.log.info(
|
std.log.info(
|
||||||
"Finished registering assets with {} blocks ({} migrations), {} items {} tools. {} biomes ({} migrations), {} recipes and {} models",
|
"Finished registering assets with {} blocks ({} migrations), {} items {} tools, {} biomes ({} migrations), {} recipes and {} models",
|
||||||
.{blocks.count(), blockMigrations.count(), items.count(), tools.count(), biomes.count(), biomeMigrations.count(), recipes.count(), models.count()},
|
.{blocks.count(), blockMigrations.count(), items.count(), tools.count(), biomes.count(), biomeMigrations.count(), recipes.count(), models.count()},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -586,6 +639,7 @@ pub fn unloadAssets() void { // MARK: unloadAssets()
|
|||||||
if(!loadedAssets) return;
|
if(!loadedAssets) return;
|
||||||
loadedAssets = false;
|
loadedAssets = false;
|
||||||
|
|
||||||
|
sbb.reset();
|
||||||
blocks_zig.reset();
|
blocks_zig.reset();
|
||||||
items_zig.reset();
|
items_zig.reset();
|
||||||
biomes_zig.reset();
|
biomes_zig.reset();
|
||||||
|
@ -16,10 +16,12 @@ const rotation = @import("rotation.zig");
|
|||||||
const RotationMode = rotation.RotationMode;
|
const RotationMode = rotation.RotationMode;
|
||||||
const Degrees = rotation.Degrees;
|
const Degrees = rotation.Degrees;
|
||||||
const Entity = main.server.Entity;
|
const Entity = main.server.Entity;
|
||||||
|
const sbb = main.server.terrain.structure_building_blocks;
|
||||||
|
|
||||||
pub const BlockTag = enum(u32) {
|
pub const BlockTag = enum(u32) {
|
||||||
air = 0,
|
air = 0,
|
||||||
fluid = 1,
|
fluid = 1,
|
||||||
|
sbbChild = 2,
|
||||||
_,
|
_,
|
||||||
|
|
||||||
var tagList: main.List([]const u8) = .init(allocator);
|
var tagList: main.List([]const u8) = .init(allocator);
|
||||||
@ -142,6 +144,12 @@ pub fn register(_: []const u8, id: []const u8, zon: ZonElement) u16 {
|
|||||||
|
|
||||||
_blockTags[size] = BlockTag.loadFromZon(allocator, zon.getChild("tags"));
|
_blockTags[size] = BlockTag.loadFromZon(allocator, zon.getChild("tags"));
|
||||||
if(_blockTags[size].len == 0) std.log.err("Block {s} is missing 'tags' field", .{id});
|
if(_blockTags[size].len == 0) std.log.err("Block {s} is missing 'tags' field", .{id});
|
||||||
|
for(_blockTags[size]) |tag| {
|
||||||
|
if(tag == BlockTag.sbbChild) {
|
||||||
|
sbb.registerChildBlock(@intCast(size), _id[size]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
_light[size] = zon.get(u32, "emittedLight", 0);
|
_light[size] = zon.get(u32, "emittedLight", 0);
|
||||||
_absorption[size] = zon.get(u32, "absorbedLight", 0xffffff);
|
_absorption[size] = zon.get(u32, "absorbedLight", 0xffffff);
|
||||||
_degradable[size] = zon.get(bool, "degradable", false);
|
_degradable[size] = zon.get(bool, "degradable", false);
|
||||||
|
149
src/server/terrain/structure_building_blocks.zig
Normal file
149
src/server/terrain/structure_building_blocks.zig
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const main = @import("root");
|
||||||
|
const ZonElement = main.ZonElement;
|
||||||
|
const Blueprint = main.blueprint.Blueprint;
|
||||||
|
const List = main.List;
|
||||||
|
const ListUnmanaged = main.ListUnmanaged;
|
||||||
|
const AliasTable = main.utils.AliasTable;
|
||||||
|
const Neighbor = main.chunk.Neighbor;
|
||||||
|
const Block = main.blocks.Block;
|
||||||
|
const Degrees = main.rotation.Degrees;
|
||||||
|
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
|
||||||
|
|
||||||
|
var arena = main.heap.NeverFailingArenaAllocator.init(main.globalAllocator);
|
||||||
|
const arenaAllocator = arena.allocator();
|
||||||
|
|
||||||
|
var blueprintCache: std.StringHashMapUnmanaged(*[4]BlueprintEntry) = .{};
|
||||||
|
var childrenToResolve: List(struct {parentId: []const u8, colorName: []const u8, colorIndex: usize, childIndex: usize, structureId: []const u8}) = undefined;
|
||||||
|
|
||||||
|
const originBlockStringId = "cubyz:sbb/origin";
|
||||||
|
var originBlockNumericId: u16 = 0;
|
||||||
|
|
||||||
|
// Maps global child block numeric ID to index used to locally represent that child block.
|
||||||
|
var childBlockNumericIdMap: std.AutoHashMapUnmanaged(u16, u16) = .{};
|
||||||
|
var childBlockStringId: ListUnmanaged([]const u8) = .{};
|
||||||
|
|
||||||
|
const BlueprintEntry = struct {
|
||||||
|
blueprint: Blueprint,
|
||||||
|
originBlock: StructureBlock,
|
||||||
|
childBlocks: []StructureBlock,
|
||||||
|
|
||||||
|
const StructureBlock = struct {
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
z: u16,
|
||||||
|
index: u16,
|
||||||
|
data: u16,
|
||||||
|
|
||||||
|
pub inline fn direction(self: StructureBlock) Neighbor {
|
||||||
|
return @enumFromInt(self.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn init(blueprint: Blueprint, stringId: []const u8) !BlueprintEntry {
|
||||||
|
var self: BlueprintEntry = .{
|
||||||
|
.blueprint = blueprint,
|
||||||
|
.originBlock = undefined,
|
||||||
|
.childBlocks = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
var hasOrigin = false;
|
||||||
|
var childBlocks: ListUnmanaged(StructureBlock) = .{};
|
||||||
|
defer childBlocks.deinit(main.stackAllocator);
|
||||||
|
|
||||||
|
for(0..blueprint.blocks.width) |x| {
|
||||||
|
for(0..blueprint.blocks.depth) |y| {
|
||||||
|
for(0..blueprint.blocks.height) |z| {
|
||||||
|
const block = blueprint.blocks.get(x, y, z);
|
||||||
|
if(isOriginBlock(block)) {
|
||||||
|
if(hasOrigin) {
|
||||||
|
std.log.err("[{s}] Multiple origin blocks found.", .{stringId});
|
||||||
|
return error.MultipleOriginBlocks;
|
||||||
|
} else {
|
||||||
|
self.originBlock = StructureBlock{
|
||||||
|
.x = @intCast(x),
|
||||||
|
.y = @intCast(y),
|
||||||
|
.z = @intCast(z),
|
||||||
|
.index = std.math.maxInt(u16),
|
||||||
|
.data = block.data,
|
||||||
|
};
|
||||||
|
hasOrigin = true;
|
||||||
|
}
|
||||||
|
} else if(isChildBlock(block)) {
|
||||||
|
const childBlockLocalId = childBlockNumericIdMap.get(block.typ) orelse return error.ChildBlockNotRecognized;
|
||||||
|
childBlocks.append(main.stackAllocator, .{
|
||||||
|
.x = @intCast(x),
|
||||||
|
.y = @intCast(y),
|
||||||
|
.z = @intCast(z),
|
||||||
|
.index = childBlockLocalId,
|
||||||
|
.data = block.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!hasOrigin) {
|
||||||
|
std.log.err("[{s}] No origin block found.", .{stringId});
|
||||||
|
return error.NoOriginBlock;
|
||||||
|
}
|
||||||
|
self.childBlocks = arenaAllocator.dupe(StructureBlock, childBlocks.items);
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn isChildBlock(block: Block) bool {
|
||||||
|
return childBlockNumericIdMap.contains(block.typ);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isOriginBlock(block: Block) bool {
|
||||||
|
return block.typ == originBlockNumericId;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn registerChildBlock(numericId: u16, stringId: []const u8) void {
|
||||||
|
const index: u16 = @intCast(childBlockNumericIdMap.count());
|
||||||
|
childBlockNumericIdMap.put(arenaAllocator.allocator, numericId, index) catch unreachable;
|
||||||
|
// Take only color name from the ID.
|
||||||
|
var iterator = std.mem.splitBackwardsScalar(u8, stringId, '/');
|
||||||
|
const colorName = iterator.first();
|
||||||
|
childBlockStringId.append(arenaAllocator, arenaAllocator.dupe(u8, colorName));
|
||||||
|
std.log.debug("Structure child block '{s}' {} ('{s}' {}) ", .{colorName, index, stringId, numericId});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn registerBlueprints(blueprints: *std.StringHashMap([]u8)) !void {
|
||||||
|
std.debug.assert(blueprintCache.capacity() == 0);
|
||||||
|
|
||||||
|
originBlockNumericId = main.blocks.parseBlock(originBlockStringId).typ;
|
||||||
|
std.log.debug("Origin block numeric id: {}", .{originBlockNumericId});
|
||||||
|
|
||||||
|
blueprintCache.ensureTotalCapacity(arenaAllocator.allocator, blueprints.count()) catch unreachable;
|
||||||
|
|
||||||
|
var iterator = blueprints.iterator();
|
||||||
|
while(iterator.next()) |entry| {
|
||||||
|
const stringId = entry.key_ptr.*;
|
||||||
|
const blueprint0 = Blueprint.load(arenaAllocator, entry.value_ptr.*) catch |err| {
|
||||||
|
std.log.err("Could not load blueprint {s}: {s}", .{stringId, @errorName(err)});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotatedBlueprints = arenaAllocator.create([4]BlueprintEntry);
|
||||||
|
rotatedBlueprints.* = .{
|
||||||
|
BlueprintEntry.init(blueprint0, stringId) catch continue,
|
||||||
|
BlueprintEntry.init(blueprint0.rotateZ(arenaAllocator, .@"90"), stringId) catch continue,
|
||||||
|
BlueprintEntry.init(blueprint0.rotateZ(arenaAllocator, .@"180"), stringId) catch continue,
|
||||||
|
BlueprintEntry.init(blueprint0.rotateZ(arenaAllocator, .@"270"), stringId) catch continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
blueprintCache.put(arenaAllocator.allocator, arenaAllocator.dupe(u8, stringId), rotatedBlueprints) catch unreachable;
|
||||||
|
std.log.debug("Registered blueprint: {s}", .{stringId});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset() void {
|
||||||
|
childBlockNumericIdMap = .{};
|
||||||
|
childBlockStringId = .{};
|
||||||
|
blueprintCache = .{};
|
||||||
|
|
||||||
|
_ = arena.reset(.free_all);
|
||||||
|
}
|
@ -20,6 +20,8 @@ pub const CaveMap = @import("CaveMap.zig");
|
|||||||
|
|
||||||
pub const StructureMap = @import("StructureMap.zig");
|
pub const StructureMap = @import("StructureMap.zig");
|
||||||
|
|
||||||
|
pub const structure_building_blocks = @import("structure_building_blocks.zig");
|
||||||
|
|
||||||
/// A generator for setting the actual Blocks in each Chunk.
|
/// A generator for setting the actual Blocks in each Chunk.
|
||||||
pub const BlockGenerator = struct {
|
pub const BlockGenerator = struct {
|
||||||
init: *const fn(parameters: ZonElement) void,
|
init: *const fn(parameters: ZonElement) void,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user