implement asset loading from bundle

This commit is contained in:
Rokas Puzonas 2026-02-16 21:30:46 +02:00
parent 6b48af7ea9
commit 483daaf91e
24 changed files with 1022 additions and 285 deletions

View File

@ -53,33 +53,60 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
}
const engine_lib = b.createModule(.{
.root_source_file = b.path("src/lib/root.zig")
.root_source_file = b.path("src/game_lib/root.zig")
});
engine_lib.addOptions("build_options", build_options);
opts.root_module.addImport("engine", engine_lib);
const runtime_module = createRuntimeModule(b, .{
const dep_stb = b.dependency("stb", .{});
engine_lib.addImport("stb_image", dep_stb.module("stb_image"));
opts.root_module.addImport("engine_lib", engine_lib);
const engine_module = createEngineModule(b, .{
.target = opts.target,
.optimize = opts.optimize,
.has_tracy = has_tracy,
.has_imgui = has_imgui
});
runtime_module.root_module.addImport("lib", engine_lib);
engine_module.root_module.addImport("engine_lib", engine_lib);
if (statically_linked) {
runtime_module.root_module.addImport("game", opts.root_module);
engine_module.root_module.addImport("game", opts.root_module);
}
const assset_bundler_tool = buildAssetBundler(
b, b.graph.host, .ReleaseSafe,
engine_module.root_module,
opts.root_module
);
const asset_bundler_step = b.addRunArtifact(assset_bundler_tool);
asset_bundler_step.addDirectoryArg(opts.asset_dir);
const assets_bundle_file = asset_bundler_step.addOutputFileArg("assets.bin");
const runtime_mod = b.createModule(.{
.root_source_file = b.path("src/runtime/main.zig"),
.target = opts.target,
.optimize = opts.optimize,
});
runtime_mod.addImport("engine", engine_module.root_module);
if (!load_assets_at_runtime) {
// TODO: Add compression on asset bundle
runtime_mod.addAnonymousImport("asset_bundle", .{
.root_source_file = assets_bundle_file
});
}
var run_cmd_step: *std.Build.Step = undefined;
if (isWasm) {
const build_step = buildWasm(b, .{
.name = "index",
.root_module = runtime_module.root_module,
.dep_sokol = runtime_module.dep_sokol,
.root_module = runtime_mod,
.dep_sokol = engine_module.dep_sokol,
.outer_builder = outer_builder
});
outer_builder.getInstallStep().dependOn(build_step);
const dep_sokol = runtime_module.dep_sokol;
const dep_sokol = engine_module.dep_sokol;
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk });
emrun_step.step.dependOn(build_step);
@ -87,10 +114,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
} else {
runtime_module.root_module.link_libc = true;
runtime_mod.link_libc = true;
const exe = buildNative(b, .{
.name = opts.name,
.root_module = runtime_module.root_module,
.root_module = runtime_mod,
.win32_has_console = opts.win32_has_console,
.win32_png_icon = opts.win32_png_icon
});
@ -123,9 +150,15 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
const run_step = outer_builder.step("run", "Run game");
run_step.dependOn(run_cmd_step);
{
const install_assets_bin = outer_builder.addInstallFileWithDir(assets_bundle_file, .bin, "assets.bin");
const assets_step = outer_builder.step("assets", "Build assets file");
assets_step.dependOn(&install_assets_bin.step);
}
}
const RuntimeModule = struct {
const EngineModule = struct {
root_module: *std.Build.Module,
dep_sokol: *std.Build.Dependency,
@ -138,9 +171,9 @@ const RuntimeModule = struct {
};
};
fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule {
fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
const mod = b.createModule(.{
.root_source_file = b.path("src/runtime/main.zig"),
.root_source_file = b.path("src/engine/root.zig"),
.target = opts.target,
.optimize = opts.optimize,
.link_libc = true
@ -199,7 +232,7 @@ fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule
mod.addIncludePath(dep_sokol_c.path("util"));
mod.addCSourceFile(.{
.file = b.path("src/runtime/fontstash/sokol_fontstash_impl.c"),
.file = b.path("src/engine/fontstash/sokol_fontstash_impl.c"),
.flags = cflags.items
});
}
@ -216,12 +249,36 @@ fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule
// // TODO: Define buid config for wasm
// }
return RuntimeModule{
return EngineModule{
.root_module = mod,
.dep_sokol = dep_sokol
};
}
fn buildAssetBundler(
b: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
engine: *std.Build.Module,
game: *std.Build.Module
) *std.Build.Step.Compile {
const mod = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = b.path("tools/asset-bundler.zig"),
});
mod.addImport("engine", engine);
mod.addImport("game", game);
// mod.addImport("asset_bundle", b.createModule(.{
// .root_source_file = b.path("src/runtime/asset_bundle.zig")
// }));
return b.addExecutable(.{
.name = "asset-bundler",
.root_module = mod,
});
}
fn buildPngToIconTool(
b: *std.Build,
target: std.Build.ResolvedTarget,
@ -310,7 +367,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step {
.use_webgl2 = true,
.use_emmalloc = true,
.use_filesystem = false,
.shell_file_path = b.path("src/runtime/shell.html"),
.shell_file_path = b.path("src/engine/shell.html"),
}) catch unreachable;
return &link_step.step;

View File

@ -0,0 +1,166 @@
const std = @import("std");
const Lib = @import("engine_lib");
const AssetBundle = @This();
fonts: std.ArrayList(Font),
sounds: std.ArrayList(Sound),
textures: std.ArrayList(Texture),
data: std.ArrayList(u8),
const DataSegment = extern struct {
offset: u64,
len: u64,
};
const Font = extern struct {
asset_id: Lib.Asset.Id,
data: DataSegment
};
const Sound = extern struct {
asset_id: Lib.Asset.Id,
data: DataSegment
};
const Texture = extern struct {
asset_id: Lib.Asset.Id,
info: Lib.Texture.Info,
data: DataSegment
};
const Header = extern struct {
magic: Magic,
fonts_len: u32,
sounds_len: u32,
textures_len: u32,
data_len: u32,
pub const Magic = enum(u32) {
v1 = 0x961C8C65,
_,
};
};
const endianness: std.builtin.Endian = .little;
pub fn init() AssetBundle {
return AssetBundle{
.fonts = .empty,
.sounds = .empty,
.textures = .empty,
.data = .empty,
};
}
pub fn deinit(self: *AssetBundle, gpa: std.mem.Allocator) void {
self.sounds.deinit(gpa);
self.textures.deinit(gpa);
self.fonts.deinit(gpa);
self.data.deinit(gpa);
}
pub fn getData(self: AssetBundle, data: DataSegment) []const u8 {
return self.data.items[data.offset..][0..data.len];
}
fn insertData(
self: *AssetBundle,
gpa: std.mem.Allocator,
data: []const u8
) !DataSegment {
const data_offset = self.data.items.len;
try self.data.appendSlice(gpa, data);
return DataSegment{
.offset = data_offset,
.len = data.len
};
}
pub fn appendSound(
self: *AssetBundle,
gpa: std.mem.Allocator,
id: Lib.Asset.Id,
data: []const u8
) !void {
try self.sounds.append(gpa, Sound{
.asset_id = id,
.data = try self.insertData(gpa, data)
});
}
pub fn appendFont(
self: *AssetBundle,
gpa: std.mem.Allocator,
id: Lib.Asset.Id,
data: []const u8
) !void {
try self.fonts.append(gpa, Font{
.asset_id = id,
.data = try self.insertData(gpa, data)
});
}
pub fn appendTexture(
self: *AssetBundle,
gpa: std.mem.Allocator,
id: Lib.Asset.Id,
data: []const u8,
info: Lib.Texture.Info
) !void {
try self.textures.append(gpa, Texture{
.asset_id = id,
.info = info,
.data = try self.insertData(gpa, data)
});
}
pub fn write(self: AssetBundle, writer: *std.Io.Writer) !void {
try writer.writeStruct(Header{
.magic = .v1,
.fonts_len = @intCast(self.fonts.items.len),
.sounds_len = @intCast(self.sounds.items.len),
.textures_len = @intCast(self.textures.items.len),
.data_len = @intCast(self.data.items.len)
}, endianness);
try writer.writeSliceEndian(Font, self.fonts.items, endianness);
try writer.writeSliceEndian(Sound, self.sounds.items, endianness);
try writer.writeSliceEndian(Texture, self.textures.items, endianness);
try writer.writeAll(self.data.items);
}
pub fn read(self: *AssetBundle, gpa: std.mem.Allocator, reader: *std.Io.Reader) !void {
const header = try reader.takeStruct(Header, endianness);
if (header.magic != .v1) {
return error.InvalidMagic;
}
try self.fonts.ensureTotalCapacity(gpa, header.fonts_len);
try self.sounds.ensureTotalCapacity(gpa, header.sounds_len);
try self.textures.ensureTotalCapacity(gpa, header.textures_len);
try self.data.ensureTotalCapacity(gpa, header.data_len);
self.fonts.clearRetainingCapacity();
self.sounds.clearRetainingCapacity();
self.textures.clearRetainingCapacity();
self.data.clearRetainingCapacity();
try reader.readSliceEndian(
Font,
self.fonts.addManyAsSliceAssumeCapacity(header.fonts_len),
endianness
);
try reader.readSliceEndian(
Sound,
self.sounds.addManyAsSliceAssumeCapacity(header.sounds_len),
endianness
);
try reader.readSliceEndian(
Texture,
self.textures.addManyAsSliceAssumeCapacity(header.textures_len),
endianness
);
try reader.readSliceAll(self.data.addManyAsSliceAssumeCapacity(header.data_len));
}

View File

@ -4,7 +4,7 @@ const assert = std.debug.assert;
const tracy = @import("tracy");
const Lib = @import("lib");
const Lib = @import("engine_lib");
const Math = Lib.Math;
const STBVorbis = @import("stb_vorbis");
@ -215,7 +215,7 @@ const ThreadState = struct {
temp_channel_buffer: []f32,
pub const SoundInstance = struct {
sound_id: Lib.SoundId,
sound_id: Lib.Sound.Id,
volume: f32 = 0,
cursor: u32 = 0,
};
@ -257,7 +257,7 @@ const SokolSetupOptions = struct {
const SoundList = Lib.GenerationalArrayList(u8, u8, Sound);
comptime {
assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.SoundId));
assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.Sound.Id));
}
gpa: std.mem.Allocator,
@ -353,7 +353,7 @@ pub fn deinit(self: *AudioSystem) void {
self.sounds.deinit(self.gpa);
}
pub fn addSound(self: *AudioSystem) !Lib.SoundId {
pub fn addSound(self: *AudioSystem) !Lib.Sound.Id {
self.sounds_mutex.lock();
defer self.sounds_mutex.unlock();
@ -363,7 +363,7 @@ pub fn addSound(self: *AudioSystem) !Lib.SoundId {
return @enumFromInt(sound_id.asInt());
}
pub fn removeSound(self: *AudioSystem, id: Lib.SoundId) void {
pub fn removeSound(self: *AudioSystem, id: Lib.Sound.Id) void {
self.sounds_mutex.lock();
defer self.sounds_mutex.unlock();
@ -388,7 +388,7 @@ pub const DataOptions = struct {
playback_style: ?PlaybackStyle = null,
};
pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !void {
pub fn setSoundData(self: *AudioSystem, id: Lib.Sound.Id, opts: DataOptions) !void {
const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound;
assert(opts.format == .vorbis);
@ -441,10 +441,6 @@ pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !voi
}
}
pub fn get(self: *AudioSystem, id: Lib.SoundId) Sound {
return self.sounds.items[@intFromEnum(id)];
}
fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channels: u32) void {
if (!self.running.load(.seq_cst)) {
return;
@ -482,7 +478,14 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channel
@memset(buffer[0..@intCast(num_frames * num_channels)], 0);
for (thread_state.instances.items) |*instance| {
const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse continue;
const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse {
log.warn("Attempt to play that doesn't exist", .{});
continue;
};
if (sound.* == .nil) {
log.warn("Attempt to play .nil sound", .{});
continue;
}
assert(sound.getSampleRate() == sample_rate); // TODO:
const sample_count = sound.streamChannels(.{

View File

@ -6,7 +6,7 @@ const sapp = sokol.app;
const simgui = sokol.imgui;
const sgl = sokol.gl;
const Lib = @import("lib");
const Lib = @import("engine_lib");
const Math = Lib.Math;
const Vec2 = Math.Vec2;
const Vec4 = Math.Vec4;
@ -57,8 +57,13 @@ const Texture = struct {
const Data = struct {
width: u32,
height: u32,
rgba: [*]u8
rgba: [*]u8,
};
fn destroy(self: *Texture) void {
sg.destroyView(self.view);
sg.destroyImage(self.image);
}
};
const Font = struct {
@ -74,11 +79,19 @@ const Font = struct {
const FontArray = Lib.GenerationalArrayList(u8, u8, Font);
comptime {
assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.FontId));
assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.Font.Id));
assert(FontArray.Id.none.asInt() == @intFromEnum(Lib.Font.Id.nil));
}
const TexturesArray = Lib.GenerationalArrayList(u8, u8, Texture);
comptime {
// TODO: This is PITA, can this be refactored?
assert(@bitSizeOf(TexturesArray.Id) == @bitSizeOf(Lib.Texture.Id));
assert(TexturesArray.Id.none.asInt() == @intFromEnum(Lib.Texture.Id.nil));
}
gpa: std.mem.Allocator,
textures: std.ArrayList(Texture) = .empty,
textures: TexturesArray = .empty,
fonts: FontArray = .empty,
scale_stack_buffer: [32]Vec2 = undefined,
@ -188,20 +201,24 @@ pub fn setupSokol(self: *Graphics) !void {
}
pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void {
if (self.sokol_state) |*sokol_state| {
imgui.shutdown();
sokol_state.font_context.deinit();
sgl.shutdown();
sg.shutdown();
}
var fonts_iter = self.fonts.iterator();
while (fonts_iter.nextItem()) |font| {
font.deinit(self.gpa);
}
self.fonts.deinit(gpa);
var textures_iter = self.textures.iterator();
while (textures_iter.nextItem()) |texture| {
texture.destroy();
}
self.textures.deinit(gpa);
if (self.sokol_state) |*sokol_state| {
imgui.shutdown();
sokol_state.font_context.deinit();
sgl.shutdown();
sg.shutdown();
}
}
pub fn drawCommand(self: *Graphics, command: Command) void {
@ -293,17 +310,6 @@ pub fn drawCommands(self: *Graphics, commands: []const Command) void {
}
}
pub fn refreshFonts(self: *Graphics) !void {
const sokol_state = self.sokol_state orelse return;
var fonts_iter = self.fonts.iterator();
while (fonts_iter.nextItem()) |font| {
if (font.fontstash_id == null and font.data != null) {
font.fontstash_id = try sokol_state.font_context.addFont("", font.data.?);
}
}
}
pub fn beginFrame(self: *Graphics) !void {
assert(self.sokol_state != null);
const sokol_state = self.sokol_state.?;
@ -378,16 +384,24 @@ const Vertex = struct {
uv: Vec2
};
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.TextureId) void {
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.Texture.Id) void {
assert(self.sokol_state != null);
const sokol_state = self.sokol_state.?;
var view: ?sg.View = null;
if (self.textures.get(.fromInt(@intFromEnum(texture_id)))) |texture| {
view = texture.view;
} else {
log.warn("Attempt to use texture that doesn't exist, id={}", .{texture_id});
}
sgl.enableTexture();
defer sgl.disableTexture();
const view = self.textures.items[@intFromEnum(texture_id)].view;
// TODO: Make sampler configurable
sgl.texture(view, sokol_state.nearest_sampler);
if (view != null) {
sgl.texture(view.?, sokol_state.nearest_sampler);
}
sgl.beginQuads();
defer sgl.end();
@ -465,17 +479,9 @@ fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
);
}
fn makeView(image: sg.Image) !sg.View {
const image_view = sg.makeView(.{
.texture = .{ .image = image }
});
if (image_view.id == sg.invalid_id) {
return error.InvalidView;
}
return image_view;
}
// ---------------------- Textures ----------------- //
fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
fn initImageWithMipMaps(image: sg.Image, mipmaps: []const Texture.Data) !void {
if (mipmaps.len == 0) {
return error.NoMipMaps;
}
@ -490,7 +496,7 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
});
}
const image = sg.makeImage(.{
sg.initImage(image, .{
.width = @intCast(mipmaps[0].width),
.height = @intCast(mipmaps[0].height),
.pixel_format = .RGBA8,
@ -500,35 +506,60 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
.num_mipmaps = @intCast(mip_levels.items.len),
.data = data
});
if (image.id == sg.invalid_id) {
return error.InvalidImage;
if (sg.queryImageState(image) != .VALID) {
return error.InitImage;
}
}
return image;
}
pub fn loadTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !Lib.TextureId {
const image = try makeImageWithMipMaps(mipmaps);
errdefer sg.deallocImage(image);
const view = try makeView(image);
errdefer sg.deallocView(view);
assert(mipmaps.len > 0);
const index = self.textures.items.len;
try self.textures.append(gpa, .{
.image = image,
.view = view,
.info = .{
.width = mipmaps[0].width,
.height = mipmaps[0].height,
}
pub fn addTexture(self: *Graphics) !Lib.Texture.Id {
const texture_id = try self.textures.insert(self.gpa, .{
.image = .{ .id = sg.invalid_id },
.view = .{ .id = sg.invalid_id }
});
return @enumFromInt(index);
return @enumFromInt(texture_id.asInt());
}
pub fn addFont(self: *Graphics) !Lib.FontId {
pub fn removeTexture(self: *Graphics, id: Lib.Texture.Id) void {
_ = self.textures.remove(id);
}
pub fn setTextureData(self: *Graphics, id: Lib.Texture.Id, data: Texture.Data) !void {
if (self.sokol_state == null) {
return error.SokolNotSetup;
}
const texture = self.textures.get(.fromInt(@intFromEnum(id))) orelse return;
if (texture.image.id == sg.invalid_id) {
texture.image = sg.allocImage();
if (texture.image.id == sg.invalid_id) {
return error.AllocImage;
}
}
if (texture.view.id == sg.invalid_id) {
texture.view = sg.allocView();
if (texture.view.id == sg.invalid_id) {
return error.AllocView;
}
}
sg.uninitView(texture.view);
sg.uninitImage(texture.image);
try initImageWithMipMaps(texture.image, &.{ data });
sg.initView(texture.view, .{
.texture = .{ .image = texture.image }
});
if (sg.queryViewState(texture.view) != .VALID) {
return error.InitView;
}
}
// ---------------------- Fonts ----------------- //
pub fn addFont(self: *Graphics) !Lib.Font.Id {
const font_id = try self.fonts.insert(self.gpa, .{
.fontstash_id = null,
.data = null
@ -536,18 +567,23 @@ pub fn addFont(self: *Graphics) !Lib.FontId {
return @enumFromInt(font_id.asInt());
}
pub fn removeFont(self: *Graphics, id: Lib.FontId) void {
pub fn removeFont(self: *Graphics, id: Lib.Font.Id) void {
_ = self.fonts.remove(.fromInt(@intFromEnum(id)));
}
pub fn setFontData(self: *Graphics, id: Lib.FontId, data: []const u8) !void {
if (self.fonts.get(.fromInt(@intFromEnum(id)))) |font| {
pub fn setFontData(self: *Graphics, id: Lib.Font.Id, data: []const u8) !void {
const sokol_state = self.sokol_state orelse return error.SokolNotSetup;
const font = self.fonts.get(.fromInt(@intFromEnum(id))) orelse return;
const new_data = try self.gpa.dupe(u8, data);
errdefer self.gpa.free(new_data);
const new_fontstash_id = try sokol_state.font_context.addFont("", new_data);
if (font.data) |old_data| {
self.gpa.free(old_data);
}
font.data = new_data;
font.fontstash_id = null;
}
font.fontstash_id = new_fontstash_id;
}

View File

@ -9,7 +9,7 @@ const sokol = @import("sokol");
const sapp = sokol.app;
const simgui = sokol.imgui;
const Lib = @import("lib");
const Lib = @import("engine_lib");
const build_options = Lib.build_options;
const enabled = build_options.has_imgui;

View File

@ -2,7 +2,7 @@ const std = @import("std");
const assert = std.debug.assert;
const sokol = @import("sokol");
const lib = @import("lib");
const lib = @import("engine_lib");
const Frame = lib.Frame;
const Nanoseconds = lib.Nanoseconds;
const Vec2 = lib.Math.Vec2;

View File

@ -5,7 +5,7 @@ const assert = std.debug.assert;
const sokol = @import("sokol");
const sapp = sokol.app;
pub const Input = @import("./input.zig");
const Input = @import("./input.zig");
const ScreenScalar = @import("./screen_scaler.zig");
pub const imgui = @import("./imgui.zig");
@ -17,7 +17,9 @@ const STBImage = @import("stb_image");
const Gfx = Graphics;
const Lib = @import("lib");
pub const Lib = @import("engine_lib");
pub const AssetBundle = @import("./asset_bundle.zig");
const build_options = Lib.build_options;
const debug_keybinds = (builtin.mode == .Debug);
const GameCallbacks = Lib.Callbacks;
@ -343,21 +345,22 @@ const GameState = struct {
}
}
fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn, initial: Init, assets: []const Lib.Asset) void {
fn runGameInit(
self: *GameState,
init_fn: GameCallbacks.InitFn,
initial: Init,
asset_registry: Lib.Asset.Registry
) void {
assert(self.frame == null);
const opts = Lib.Init{
.gpa = self.gpa,
.seed = initial.seed,
.assets = .{
.items = assets
}
.assets = asset_registry
};
self.state = init_fn(&opts);
self.frame = .init(self.gpa, initial.screen_size);
self.frame.?.assets = .{
.items = assets
};
self.frame.?.assets = asset_registry;
}
fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void {
@ -476,13 +479,157 @@ const Recording = struct {
}
};
const AssetManager = struct {
const LoadAsset = union(enum) {
font: struct {
id: Lib.Font.Id,
path: []const u8
},
sound: struct {
id: Lib.Sound.Id,
path: []const u8
},
texture: struct {
id: Lib.Texture.Id,
path: []const u8
},
pub fn clone(self: LoadAsset, arena: std.mem.Allocator) !LoadAsset {
return switch (self) {
.font => |opts| LoadAsset{
.font = .{
.id = opts.id,
.path = try arena.dupe(u8, opts.path)
}
},
.sound => |opts| LoadAsset{
.sound = .{
.id = opts.id,
.path = try arena.dupe(u8, opts.path)
}
},
.texture => |opts| LoadAsset{
.texture = .{
.id = opts.id,
.path = try arena.dupe(u8, opts.path)
}
}
};
}
};
gpa: std.mem.Allocator,
assets: std.ArrayList(Lib.Asset),
textures: std.ArrayList(Lib.Texture),
dir: ?std.fs.Dir,
queued_loads_arena: std.heap.ArenaAllocator,
queued_loads: std.ArrayList(LoadAsset),
pub fn init(gpa: std.mem.Allocator, dir: ?std.fs.Dir) AssetManager {
return AssetManager{
.gpa = gpa,
.assets = .empty,
.textures = .empty,
.dir = dir,
.queued_loads_arena = .init(gpa),
.queued_loads = .empty
};
}
pub fn deinit(self: *AssetManager) void {
self.queued_loads_arena.deinit();
self.assets.deinit(self.gpa);
self.textures.deinit(self.gpa);
}
pub fn initRegistry(self: AssetManager) Lib.Asset.Registry {
return Lib.Asset.Registry{
.assets = self.assets.items,
.textures = self.textures.items
};
}
pub fn enqueueLoad(self: *AssetManager, load: LoadAsset) !void {
const arena = self.queued_loads_arena.allocator();
try self.queued_loads.append(arena, try load.clone(arena));
}
pub fn performQueuedLoads(self: *AssetManager, graphics: *Graphics, audio: *Audio) !void {
if (self.dir == null) {
if (self.queued_loads.items.len > 0) {
return error.MissingAssetDirectory;
}
return;
}
const dir = self.dir.?;
for (self.queued_loads.items) |queued_load| {
switch (queued_load) {
.font => |opts| {
const font_data = try readFileAll(self.gpa, dir, opts.path);
defer self.gpa.free(font_data);
try graphics.setFontData(opts.id, font_data);
},
.sound => |opts| {
const sound_data = try readFileAll(self.gpa, dir, opts.path);
defer self.gpa.free(sound_data);
try audio.setSoundData(opts.id, .{
.data = sound_data,
.format = .vorbis
});
},
.texture => |opts| {
const image_data = try readFileAll(self.gpa, dir, opts.path);
defer self.gpa.free(image_data);
const image = try STBImage.load(image_data);
defer image.deinit();
// TODO: Assert that loaded data matches precomputed texture info
try graphics.setTextureData(opts.id, .{
.width = image.width,
.height = image.height,
.rgba = image.rgba8_pixels,
});
}
}
}
_ = self.queued_loads_arena.reset(.free_all);
self.queued_loads = .empty;
}
fn hasTexture(self: AssetManager, texture_id: Lib.Texture.Id) bool {
for (self.textures.items) |texture| {
if (texture.id == texture_id) {
return true;
}
}
return false;
}
pub fn assertAssetsLoaded(self: AssetManager) void {
for (self.assets.items) |asset| {
switch (asset.kind) {
.texture => |texture_id| {
assert(self.hasTexture(texture_id));
},
else => {} // TODO:
}
}
}
};
const Engine = @This();
allocator: std.mem.Allocator,
graphics: Graphics,
audio: Audio,
assets: std.ArrayList(Lib.Asset),
asset_bundle: ?AssetBundle = null,
asset_manager: AssetManager,
game_linking: GameLinking,
game_state: GameState,
@ -501,6 +648,7 @@ recording_tick: u64,
const RunOptions = struct {
allocator: std.mem.Allocator,
game_library_path: ?[]const u8 = null,
asset_bundle_bytes: ?[]const u8 = null,
do_recording: bool = (builtin.mode == .Debug),
screen_width: u32 = 640,
screen_height: u32 = 480,
@ -514,11 +662,30 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
game_linking = try .initStatic();
}
var maybe_asset_bundle: ?AssetBundle = null;
if (opts.asset_bundle_bytes) |asset_bundle_bytes| {
var asset_bundle = AssetBundle.init();
errdefer asset_bundle.deinit(opts.allocator);
var reader = std.Io.Reader.fixed(asset_bundle_bytes);
asset_bundle.read(opts.allocator, &reader) catch |e| {
log.err("Failed to load asset bundle: {}", .{e});
return error.FailedToLoadAssetBundle;
};
maybe_asset_bundle = asset_bundle;
}
const initial = GameState.Init{
.seed = @bitCast(std.time.milliTimestamp()),
.screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height)
};
var asset_dir: ?std.fs.Dir = null;
if (build_options.load_assets_at_runtime) {
asset_dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true });
}
self.* = Engine{
.allocator = opts.allocator,
.graphics = try .init(.{
@ -543,64 +710,100 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
.play_recording = false,
.recording_tick = 0,
.seed = initial.seed,
.assets = .empty
.asset_manager = .init(opts.allocator, asset_dir),
.asset_bundle = maybe_asset_bundle,
};
if (build_options.load_assets_at_runtime) {
var dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true });
defer dir.close();
const dir = asset_dir.?;
var loader: Lib.Asset.Loader = .init(opts.allocator, dir);
defer loader.deinit();
game_linking.callbacks.loadAssets(&loader);
for (loader.assets.items) |asset_desc| {
switch (asset_desc.kind) {
.font => |path| {
const f = try dir.openFile(path, .{});
defer f.close();
var wa: std.Io.Writer.Allocating = .init(self.allocator);
defer wa.deinit();
var buffer: [4 * 4096]u8 = undefined;
var reader = f.reader(&buffer);
_ = try reader.interface.stream(&wa.writer, .unlimited);
const font_id = try self.graphics.addFont();
try self.graphics.setFontData(font_id, wa.written());
try self.assets.append(self.allocator, .{
try self.asset_manager.assets.append(self.allocator, .{
.id = asset_desc.id,
.kind = .{ .font = font_id }
});
try self.asset_manager.enqueueLoad(.{
.font = .{
.id = font_id,
.path = path
}
});
},
.texture => |path| {
_ = path; // autofix
.texture => |texture| {
const texture_id = try self.graphics.addTexture();
try self.asset_manager.assets.append(self.allocator, .{
.id = asset_desc.id,
.kind = .{ .texture = texture_id }
});
try self.asset_manager.enqueueLoad(.{
.texture = .{
.id = texture_id,
.path = texture.path
}
});
try self.asset_manager.textures.append(self.asset_manager.gpa, .{
.id = texture_id,
.info = texture.info
});
},
.sound => |path| {
const f = try dir.openFile(path, .{});
defer f.close();
var wa: std.Io.Writer.Allocating = .init(self.allocator);
defer wa.deinit();
var buffer: [4 * 4096]u8 = undefined;
var reader = f.reader(&buffer);
_ = try reader.interface.stream(&wa.writer, .unlimited);
const sound_id = try self.audio.addSound();
try self.audio.setSoundData(sound_id, .{
.data = wa.written(),
.format = .vorbis
});
try self.assets.append(self.allocator, .{
try self.asset_manager.assets.append(self.allocator, .{
.id = asset_desc.id,
.kind = .{ .sound = sound_id }
});
try self.asset_manager.enqueueLoad(.{
.sound = .{
.id = sound_id,
.path = path
}
});
},
}
}
}
self.game_state.runGameInit(game_linking.callbacks.init, initial, self.assets.items);
if (maybe_asset_bundle) |asset_bundle| {
for (asset_bundle.fonts.items) |font| {
const font_id = try self.graphics.addFont();
try self.asset_manager.assets.append(self.allocator, .{
.id = font.asset_id,
.kind = .{ .font = font_id }
});
}
for (asset_bundle.textures.items) |texture| {
const texture_id = try self.graphics.addTexture();
try self.asset_manager.assets.append(self.allocator, .{
.id = texture.asset_id,
.kind = .{ .texture = texture_id }
});
try self.asset_manager.textures.append(self.asset_manager.gpa, .{
.id = texture_id,
.info = texture.info
});
}
for (asset_bundle.sounds.items) |sound| {
const sound_id = try self.audio.addSound();
try self.asset_manager.assets.append(self.allocator, .{
.id = sound.asset_id,
.kind = .{ .sound = sound_id }
});
}
}
self.game_state.runGameInit(game_linking.callbacks.init, initial, self.asset_manager.initRegistry());
if (opts.do_recording) {
self.recording = .init(opts.allocator, initial);
@ -660,6 +863,20 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
});
}
fn readFileAll(gpa: std.mem.Allocator, dir: std.fs.Dir, sub_path: []const u8) ![]u8 {
const f = try dir.openFile(sub_path, .{});
defer f.close();
var wa: std.Io.Writer.Allocating = .init(gpa);
defer wa.deinit();
var buffer: [4 * 4096]u8 = undefined;
var reader = f.reader(&buffer);
_ = try reader.interface.stream(&wa.writer, .unlimited);
return try wa.toOwnedSlice();
}
fn sokolInit(self: *Engine) !void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
@ -667,7 +884,44 @@ fn sokolInit(self: *Engine) !void {
try self.graphics.setupSokol();
self.audio.setupSokol();
try self.graphics.refreshFonts();
try self.asset_manager.performQueuedLoads(&self.graphics, &self.audio);
if (self.asset_bundle) |*asset_bundle| {
const registry = self.asset_manager.initRegistry();
for (asset_bundle.fonts.items) |font| {
const font_id = registry.getFont(font.asset_id);
assert(font_id != Lib.Font.Id.nil);
try self.graphics.setFontData(font_id, asset_bundle.getData(font.data));
}
for (asset_bundle.textures.items) |texture| {
const texture_id = registry.getTexture(texture.asset_id);
assert(texture_id != Lib.Texture.Id.nil);
const image = try STBImage.load(asset_bundle.getData(texture.data));
defer image.deinit();
try self.graphics.setTextureData(texture_id, .{
.width = image.width,
.height = image.height,
.rgba = image.rgba8_pixels
});
}
for (asset_bundle.sounds.items) |sound| {
const sound_id = registry.getSound(sound.asset_id);
assert(sound_id != Lib.Sound.Id.nil);
try self.audio.setSoundData(sound_id, .{
.data = asset_bundle.getData(sound.data),
.format = .vorbis
});
}
asset_bundle.deinit(self.allocator);
self.asset_bundle = null;
}
self.asset_manager.assertAssetsLoaded();
}
fn sokolCleanup(self: *Engine) void {
@ -684,7 +938,7 @@ fn sokolCleanup(self: *Engine) void {
self.audio.deinit();
self.graphics.deinit(self.allocator);
self.game_linking.deinit(self.allocator);
self.assets.deinit(self.allocator);
self.asset_manager.deinit();
}
fn sokolFrame(self: *Engine) !void {
@ -759,7 +1013,7 @@ fn sokolFrame(self: *Engine) !void {
self.game_state.deinit();
self.game_state = .init(self.allocator);
self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.assets.items);
self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.asset_manager.initRegistry());
if (self.recording) |recording| {
recording.deinit();
@ -811,7 +1065,7 @@ fn showDebugWindow(self: *Engine) !void {
self.game_state.deinit();
self.game_state = .init(self.allocator);
self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.assets.items);
self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.asset_manager.initRegistry());
self.play_recording = true;
self.recording_tick = 0;

View File

@ -1,11 +1,11 @@
const Gfx = @import("./graphics.zig");
const Math = @import("lib").Math;
const Math = @import("engine_lib").Math;
const Vec2 = Math.Vec2;
const Vec4 = Math.Vec4;
const rgb = Math.rgb;
const Lib = @import("lib");
const Lib = @import("engine_lib");
const Frame = Lib.Frame;
const ScreenScalar = @This();

View File

@ -0,0 +1,206 @@
const std = @import("std");
const log = std.log.scoped(.engine);
const STBImage = @import("stb_image");
const Font = @import("./font.zig");
const Sound = @import("./sound.zig");
const Texture = @import("./texture.zig");
const Asset = @This();
id: Id,
kind: union(Type) {
font: Font.Id,
sound: Sound.Id,
texture: Texture.Id,
},
pub const Id = u32;
pub const Type = enum {
font,
sound,
texture
};
pub const Loader = struct {
const Descriptor = struct {
const KindTexture = struct {
path: []const u8,
info: Texture.Info
};
id: Id,
kind: union(Type) {
font: []const u8,
sound: []const u8,
texture: KindTexture
}
};
arena: std.heap.ArenaAllocator,
dir: std.fs.Dir,
assets: std.ArrayList(Descriptor),
add_failed: bool,
pub fn init(gpa: std.mem.Allocator, dir: std.fs.Dir) Loader {
return Loader{
.arena = .init(gpa),
.dir = dir,
.assets = .empty,
.add_failed = false
};
}
pub fn deinit(self: *Loader) void {
self.arena.deinit();
self.* = undefined;
}
fn readFile(self: *Loader, gpa: std.mem.Allocator, path: []const u8) ![]u8 {
const f = try self.dir.openFile(path, .{});
defer f.close();
var wa: std.Io.Writer.Allocating = .init(gpa);
defer wa.deinit();
var buffer: [4 * 4096]u8 = undefined;
var reader = f.reader(&buffer);
_ = try reader.interface.stream(&wa.writer, .unlimited);
return try wa.toOwnedSlice();
}
pub fn addFont(self: *Loader, id: Asset.Id, path: []const u8) void {
const allocator = self.arena.allocator();
const path_dupe = allocator.dupe(u8, path) catch |e| {
log.err("Failed to duplicate path: {}", .{e});
return;
};
self.assets.append(allocator, Descriptor{
.id = id,
.kind = .{ .font = path_dupe }
}) catch |e| {
log.err("Failed add sound: {}", .{e});
};
}
pub fn addSound(self: *Loader, id: Asset.Id, path: []const u8) void {
const allocator = self.arena.allocator();
const path_dupe = allocator.dupe(u8, path) catch |e| {
log.err("Failed to duplicate path: {}", .{e});
return;
};
self.assets.append(allocator, Descriptor{
.id = id,
.kind = .{ .sound = path_dupe }
}) catch |e| {
log.err("Failed add sound: {}", .{e});
};
}
fn addTextureFallible(self: *Loader, id: Asset.Id, path: []const u8) !void {
const allocator = self.arena.allocator();
var image_info: Texture.Info = undefined;
{
const file_contents = try self.readFile(allocator, path);
defer allocator.free(file_contents);
const image = try STBImage.load(file_contents);
defer image.deinit();
image_info = .{
.width = image.width,
.height = image.height,
};
}
const path_dupe = allocator.dupe(u8, path) catch |e| {
log.err("Failed to duplicate path: {}", .{e});
return;
};
const texture = Descriptor.KindTexture{
.path = path_dupe,
.info = image_info
};
self.assets.append(allocator, Descriptor{
.id = id,
.kind = .{ .texture = texture }
}) catch |e| {
log.err("Failed add sound: {}", .{e});
};
}
pub fn addTexture(self: *Loader, id: Asset.Id, path: []const u8) void {
self.addTextureFallible(id, path) catch |e| {
log.err("Failed to add texture: {}", .{e});
self.add_failed = true;
};
}
};
pub const Registry = struct {
assets: []const Asset,
textures: []const Texture,
pub const empty = Registry{
.assets = &.{},
.textures = &.{}
};
pub fn getAsset(self: Registry, id: Asset.Id) ?Asset {
// TODO: Make this O(1)?
for (self.assets) |asset| {
if (asset.id == id) {
return asset;
}
}
return null;
}
pub fn getSound(self: Registry, id: Asset.Id) Sound.Id {
const asset = self.getAsset(id) orelse return Sound.Id.nil;
if (asset.kind != .sound) {
return Sound.Id.nil;
}
return asset.kind.sound;
}
pub fn getFont(self: Registry, id: Asset.Id) Font.Id {
const asset = self.getAsset(id) orelse return Font.Id.nil;
if (asset.kind != .font) {
return Font.Id.nil;
}
return asset.kind.font;
}
pub fn getTexture(self: Registry, id: Asset.Id) Texture.Id {
const asset = self.getAsset(id) orelse return Texture.Id.nil;
if (asset.kind != .texture) {
return Texture.Id.nil;
}
return asset.kind.texture;
}
pub fn getTextureInfo(self: Registry, id: Texture.Id) Texture.Info {
if (id == .nil) {
log.warn("Failed to access .nil texture info", .{});
return .nil;
}
// TODO: Make this O(1)?
for (self.textures) |texture| {
if (texture.id == id) {
return texture.info;
}
}
log.warn("Failed to find texture info, id={}", .{id});
return .nil;
}
};

View File

@ -0,0 +1,9 @@
const std = @import("std");
id: Id,
pub const Id = enum(u16) {
_,
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
};

View File

@ -6,8 +6,9 @@ const Allocator = std.mem.Allocator;
/// This array provides:
/// - O(1) insertion
/// - O(1) deletion
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
/// TODO: Use a stack of unused indexes to implement O(1) insertion
/// TODO: Implement a way to use a list *assumeCapacity() style of functions.
pub fn GenerationalArrayList(
Index: type,
Generation: type,

View File

@ -367,6 +367,11 @@ pub const Rect = struct {
.size = Vec2.zero
};
pub const unit = Rect{
.pos = Vec2.zero,
.size = Vec2.init(1, 1)
};
pub fn init(x: f32, y: f32, width: f32, height: f32) Rect {
return Rect{
.pos = Vec2.init(x, y),

View File

@ -5,6 +5,7 @@ pub const build_options = @import("build_options");
pub const ImGui = @import("./imgui.zig");
pub const Math = @import("./math.zig");
pub const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
const STBImage = @import("stb_image");
const rgb = Math.rgb;
const Rect = Math.Rect;
@ -142,24 +143,13 @@ pub const MouseButton = enum {
middle,
};
pub const TextureId = enum(u16) {
_,
};
pub const FontId = enum(u16) {
_,
pub const nil: FontId = @enumFromInt(std.math.maxInt(@typeInfo(FontId).@"enum".tag_type));
};
pub const SoundId = enum(u16) {
_,
pub const nil: SoundId = @enumFromInt(std.math.maxInt(@typeInfo(SoundId).@"enum".tag_type));
};
pub const Texture = @import("./texture.zig");
pub const Font = @import("./font.zig");
pub const Sound = @import("./sound.zig");
pub const Asset = @import("./asset.zig");
pub const Sprite = struct {
texture: TextureId,
texture: Texture.Id,
uv: Rect
};
@ -221,7 +211,7 @@ pub const GraphicsCommand = union(enum) {
pub const DrawText = struct {
pos: Vec2,
text: []const u8,
font: FontId,
font: Font.Id,
size: f32,
color: Vec4,
};
@ -240,7 +230,7 @@ pub const GraphicsCommand = union(enum) {
pub const AudioCommand = union(enum) {
pub const Play = struct {
id: SoundId,
id: Sound.Id,
volume: f32 = 1
};
@ -412,7 +402,7 @@ pub const Frame = struct {
}
pub const DrawTextOptions = struct {
font: FontId,
font: Font.Id,
size: f32 = 16,
color: Vec4 = rgb(255, 255, 255),
};
@ -491,121 +481,6 @@ pub const Init = struct {
assets: Asset.Registry,
};
pub const Asset = struct {
id: Id,
kind: union(Type) {
font: FontId,
sound: SoundId,
texture: TextureId,
},
pub const Id = u32;
pub const Type = enum {
font,
sound,
texture
};
pub const Loader = struct {
const Descriptor = struct {
id: Id,
kind: union(Type) {
font: []const u8,
sound: []const u8,
texture: []const u8,
}
};
arena: std.heap.ArenaAllocator,
dir: std.fs.Dir,
assets: std.ArrayList(Descriptor),
pub fn init(gpa: std.mem.Allocator, dir: std.fs.Dir) Loader {
return Loader{
.arena = .init(gpa),
.dir = dir,
.assets = .empty,
};
}
pub fn deinit(self: *Loader) void {
self.arena.deinit();
self.* = undefined;
}
pub fn addFont(self: *Loader, id: Asset.Id, path: []const u8) void {
const allocator = self.arena.allocator();
const path_dupe = allocator.dupe(u8, path) catch |e| {
log.err("Failed to duplicate path: {}", .{e});
return;
};
self.assets.append(allocator, Descriptor{
.id = id,
.kind = .{ .font = path_dupe }
}) catch |e| {
log.err("Failed add sound: {}", .{e});
};
}
pub fn addSound(self: *Loader, id: Asset.Id, path: []const u8) void {
const allocator = self.arena.allocator();
const path_dupe = allocator.dupe(u8, path) catch |e| {
log.err("Failed to duplicate path: {}", .{e});
return;
};
self.assets.append(allocator, Descriptor{
.id = id,
.kind = .{ .sound = path_dupe }
}) catch |e| {
log.err("Failed add sound: {}", .{e});
};
}
};
pub const Registry = struct {
items: []const Asset,
pub const empty = Registry{
.items = &.{}
};
pub fn get(self: Registry, id: Id) ?Asset {
for (self.items) |asset| {
if (asset.id == id) {
return asset;
}
}
return null;
}
pub fn getSound(self: Registry, id: Id) SoundId {
const asset = self.get(id) orelse return SoundId.nil;
if (asset.kind != Type.sound) {
return SoundId.nil;
}
return asset.kind.sound;
}
pub fn getFont(self: Registry, id: Id) FontId {
const asset = self.get(id) orelse return FontId.nil;
if (asset.kind != Type.font) {
return FontId.nil;
}
return asset.kind.font;
}
pub fn getTexture(self: Registry, id: Id) TextureId {
const asset = self.get(id) orelse return TextureId.nil;
if (asset.kind != Type.font) {
return TextureId.nil;
}
return asset.kind.font;
}
};
};
pub const Callbacks = struct {
pub const State = *anyopaque;

View File

@ -0,0 +1,9 @@
const std = @import("std");
id: Id,
pub const Id = enum(u16) {
_,
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
};

View File

@ -0,0 +1,20 @@
const std = @import("std");
id: Id,
info: Info,
pub const Info = extern struct {
width: u32,
height: u32,
pub const nil = Info{
.width = 0,
.height = 0
};
};
pub const Id = enum(u16) {
_,
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
};

View File

@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
const Engine = @import("./engine.zig");
const Engine = @import("engine");
const build_options = Engine.Lib.build_options;
var engine: Engine = undefined;
@ -8,6 +9,11 @@ pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
var asset_bundle_bytes: ?[]const u8 = null;
if (!build_options.load_assets_at_runtime) {
asset_bundle_bytes = @embedFile("asset_bundle");
}
// TODO: Use tracy TracingAllocator
var allocator: std.mem.Allocator = undefined;
if (builtin.cpu.arch.isWasm()) {
@ -28,7 +34,8 @@ pub fn main() !void {
try engine.run(.{
.allocator = allocator,
.game_library_path = game_library_path
.game_library_path = game_library_path,
.asset_bundle_bytes = asset_bundle_bytes
});
}

View File

@ -0,0 +1,79 @@
const std = @import("std");
const Game = @import("game");
const Engine = @import("engine");
const Lib = Engine.Lib;
const AssetBundle = Engine.AssetBundle;
fn readFileAll(gpa: std.mem.Allocator, dir: std.fs.Dir, sub_path: []const u8) ![]u8 {
const f = try dir.openFile(sub_path, .{});
defer f.close();
var wa: std.Io.Writer.Allocating = .init(gpa);
defer wa.deinit();
var buffer: [4 * 4096]u8 = undefined;
var reader = f.reader(&buffer);
_ = try reader.interface.stream(&wa.writer, .unlimited);
return try wa.toOwnedSlice();
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len != 3) {
std.debug.print("Usage: ./asset-bundler <asset-dir> <output>", .{});
std.process.exit(1);
}
const cwd = std.fs.cwd();
const asset_dir_path = args[1];
const output_path = args[2];
const asset_dir = try cwd.openDir(asset_dir_path, .{ .iterate = true });
var loader: Lib.Asset.Loader = .init(allocator, asset_dir);
defer loader.deinit();
Game.loadAssets(&loader);
var bundle = AssetBundle.init();
defer bundle.deinit(allocator);
for (loader.assets.items) |asset_desc| {
const id = asset_desc.id;
switch (asset_desc.kind) {
.font => |path| {
const font_data = try readFileAll(allocator, asset_dir, path);
defer allocator.free(font_data);
try bundle.appendFont(allocator, id, font_data);
},
.sound => |path| {
const sound_data = try readFileAll(allocator, asset_dir, path);
defer allocator.free(sound_data);
try bundle.appendSound(allocator, id, sound_data);
},
.texture => |opts| {
const texture_data = try readFileAll(allocator, asset_dir, opts.path);
defer allocator.free(texture_data);
try bundle.appendTexture(allocator, id, texture_data, opts.info);
}
}
}
const output_file = try std.fs.cwd().createFile(output_path, .{ });
defer output_file.close();
var buffer: [4096 * 4]u8 = undefined;
var writer = output_file.writer(&buffer);
try bundle.write(&writer.interface);
try writer.interface.flush();
}

View File

@ -2,8 +2,8 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const Engine = @import("engine");
const FontId = Engine.FontId;
const Engine = @import("engine_lib");
const FontId = Engine.Font.Id;
const Vec2 = Engine.Math.Vec2;
const rgb = Engine.Math.rgb;
@ -20,6 +20,7 @@ const AssetId = enum {
font_bold,
font_italic,
sound_wood,
texture_icon,
};
const State = struct {
@ -54,6 +55,8 @@ pub fn loadAssets(assets: *Engine.Asset.Loader) callconv(.c) void {
assets.addFont(@intFromEnum(AssetId.font_italic), "roboto-font/Roboto-Italic.ttf");
assets.addSound(@intFromEnum(AssetId.sound_wood), "wood01.ogg");
assets.addTexture(@intFromEnum(AssetId.texture_icon), "icon.png");
}
pub fn deinit(state_ptr: *anyopaque) callconv(.c) void {
@ -98,13 +101,20 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
.color = rgb(20, 20, 20)
});
const icon = frame.assets.getTexture(@intFromEnum(AssetId.texture_icon));
const size = Vec2.init(20, 20);
frame.drawRectangle(.{
.rect = .{
.pos = state.player.sub(size.divideScalar(2)),
.size = size
},
.color = rgb(200, 2, 200)
.color = rgb(200, 2, 200),
.sprite = .{
.texture = icon,
.uv = .unit
}
});
if (dir.x != 0 or dir.y != 0) {
frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3);