add cli argument parsing

This commit is contained in:
Rokas Puzonas 2026-02-18 01:03:01 +02:00
parent e995b55ccc
commit b901c474f0
8 changed files with 526 additions and 137 deletions

View File

@ -21,7 +21,8 @@ pub fn build(b: *std.Build) !void {
.has_tracy = b.option(bool, "tracy", "Tracy integration"), .has_tracy = b.option(bool, "tracy", "Tracy integration"),
.win32_has_console = b.option(bool, "console", "Show console (Window only)"), .win32_has_console = b.option(bool, "console", "Show console (Window only)"),
.win32_png_icon = b.path("src/assets/icon.png"), .win32_png_icon = b.path("src/assets/icon.png"),
.hot_reload = b.option(bool, "hot-reload", "Dynamically load game code at runtime"), .code_hot_reload = b.option(bool, "code-hot-reload", "Dynamically load game code at runtime"),
.asset_hot_reload = b.option(bool, "asset-hot-reload", "Load assets at runtime"),
.asset_dir = b.path("src/assets/"), .asset_dir = b.path("src/assets/"),
}); });

View File

@ -15,12 +15,12 @@ const InitOptions = struct {
dep_engine: *std.Build.Dependency, dep_engine: *std.Build.Dependency,
statically_linked: ?bool = null, statically_linked: ?bool = null,
hot_reload: ?bool = null, code_hot_reload: ?bool = null,
has_imgui: ?bool = null, has_imgui: ?bool = null,
has_tracy: ?bool = null, has_tracy: ?bool = null,
win32_has_console: ?bool = null, win32_has_console: ?bool = null,
win32_png_icon: ?std.Build.LazyPath = null, win32_png_icon: ?std.Build.LazyPath = null,
load_assets_at_runtime: ?bool = null, asset_hot_reload: ?bool = null,
asset_dir: std.Build.LazyPath, asset_dir: std.Build.LazyPath,
}; };
@ -31,12 +31,12 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
const isDebug = (opts.optimize == .Debug); const isDebug = (opts.optimize == .Debug);
var has_tracy = false; var has_tracy = false;
var hot_reload = false; var code_hot_reload = false;
var load_assets_at_runtime = false; var asset_hot_reload = false;
if (!isWasm) { if (!isWasm) {
hot_reload = opts.hot_reload orelse isDebug; code_hot_reload = opts.code_hot_reload orelse isDebug;
has_tracy = opts.has_tracy orelse isDebug; has_tracy = opts.has_tracy orelse isDebug;
load_assets_at_runtime = opts.load_assets_at_runtime orelse isDebug; asset_hot_reload = opts.asset_hot_reload orelse isDebug;
} }
const has_imgui = opts.has_imgui orelse isDebug; const has_imgui = opts.has_imgui orelse isDebug;
@ -45,10 +45,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
var build_options = b.addOptions(); var build_options = b.addOptions();
build_options.addOption(bool, "has_imgui", has_imgui); build_options.addOption(bool, "has_imgui", has_imgui);
build_options.addOption(bool, "has_tracy", has_tracy); build_options.addOption(bool, "has_tracy", has_tracy);
build_options.addOption(bool, "hot_reload", hot_reload); build_options.addOption(bool, "code_hot_reload", code_hot_reload);
build_options.addOption(bool, "statically_linked", statically_linked); build_options.addOption(bool, "statically_linked", statically_linked);
build_options.addOption(bool, "load_assets_at_runtime", load_assets_at_runtime); build_options.addOption(bool, "asset_hot_reload", asset_hot_reload);
if (load_assets_at_runtime) { if (asset_hot_reload) {
build_options.addOptionPath("asset_dir", opts.asset_dir); build_options.addOptionPath("asset_dir", opts.asset_dir);
} }
@ -73,6 +73,8 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
engine_module.root_module.addImport("game", opts.root_module); engine_module.root_module.addImport("game", opts.root_module);
} }
// TODO: Fix windows cross compile build.
// Fails to build asset bundler, because engine module is built for target and not host
const assset_bundler_tool = buildAssetBundler( const assset_bundler_tool = buildAssetBundler(
b, b.graph.host, .ReleaseSafe, b, b.graph.host, .ReleaseSafe,
engine_module.root_module, engine_module.root_module,
@ -89,7 +91,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
.optimize = opts.optimize, .optimize = opts.optimize,
}); });
runtime_mod.addImport("engine", engine_module.root_module); runtime_mod.addImport("engine", engine_module.root_module);
if (!load_assets_at_runtime) { if (!asset_hot_reload) {
// TODO: Add compression on asset bundle // TODO: Add compression on asset bundle
runtime_mod.addAnonymousImport("asset_bundle", .{ runtime_mod.addAnonymousImport("asset_bundle", .{
.root_source_file = assets_bundle_file .root_source_file = assets_bundle_file
@ -128,7 +130,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
if (outer_builder.args) |args| { if (outer_builder.args) |args| {
run_cmd.addArgs(args); run_cmd.addArgs(args);
} }
if (hot_reload) { if (code_hot_reload) {
opts.root_module.resolved_target = opts.target; opts.root_module.resolved_target = opts.target;
opts.root_module.optimize = opts.optimize; opts.root_module.optimize = opts.optimize;
const game_lib = b.addLibrary(.{ const game_lib = b.addLibrary(.{
@ -138,6 +140,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
}); });
const install_game_lib = b.addInstallArtifact(game_lib, .{}); const install_game_lib = b.addInstallArtifact(game_lib, .{});
run_cmd.addArg("--game-lib-path");
run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename)); run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename));
run_cmd.step.dependOn(&install_game_lib.step); run_cmd.step.dependOn(&install_game_lib.step);

300
engine/src/engine/cli.zig Normal file
View File

@ -0,0 +1,300 @@
const std = @import("std");
const log = std.log.scoped(.engine);
const CLI = @This();
gpa: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
options: std.ArrayList(Option),
commands: std.ArrayList(Command),
stdout: Channel,
stderr: Channel,
pub const Command = struct {
name: []const u8,
description: []const u8,
const Id = enum (u32) { _ };
};
pub const Option = struct {
long_name: []const u8,
description: []const u8,
kind: Kind,
pub const Kind = enum {
toggle,
single_argument
};
pub const Id = enum (u32) { _ };
const Value = struct {
argument: ?[]const u8 = null
};
};
const Channel = struct {
file: std.fs.File,
buffer: [4 * 4096]u8,
writer: std.fs.File.Writer,
pub fn init(self: *Channel, file: std.fs.File) void {
self.* = Channel{
.file = file,
.buffer = undefined,
.writer = file.writer(&self.buffer)
};
}
pub fn write(self: *Channel, comptime fmt: []const u8, args: anytype) void {
self.writer.interface.print(fmt, args) catch |e| {
log.err("Failed to write to channel: {}", .{e});
};
}
pub fn flush(self: *Channel) void {
self.writer.interface.flush() catch |e| {
log.err("Failed to flush channel: {}", .{e});
};
}
};
pub fn init(self: *CLI, gpa: std.mem.Allocator) void {
self.* = CLI{
.gpa = gpa,
.arena = .init(gpa),
.options = .empty,
.commands = .empty,
.stdout = undefined,
.stderr = undefined
};
self.stdout.init(std.fs.File.stdout());
self.stderr.init(std.fs.File.stderr());
}
pub fn deinit(self: *CLI) void {
self.stderr.flush();
self.stdout.flush();
self.arena.deinit();
self.options.deinit(self.gpa);
self.commands.deinit(self.gpa);
}
pub const AddOptionOptions = struct {
long_name: []const u8,
description: ?[]const u8 = null,
kind: Option.Kind = .toggle,
};
pub fn addOption(self: *CLI, opts: AddOptionOptions) !Option.Id {
const arena = self.arena.allocator();
var owned_description: []const u8 = "";
if (opts.description) |description| {
owned_description = try arena.dupe(u8, description);
}
const index = self.options.items.len;
try self.options.append(self.gpa, .{
.long_name = try arena.dupe(u8, opts.long_name),
.description = owned_description,
.kind = opts.kind
});
return @enumFromInt(index);
}
fn getOptionByLongName(self: *CLI, long_name: []const u8) ?Option.Id {
for (0.., self.options.items) |i, option| {
if (std.mem.eql(u8, long_name, option.long_name)) {
return @enumFromInt(i);
}
}
return null;
}
pub const AddCommandOptions = struct {
name: []const u8,
description: ?[]const u8 = null
};
pub fn addCommand(self: *CLI, opts: AddCommandOptions) !Command.Id {
const arena = self.arena.allocator();
var owned_description: []const u8 = "";
if (opts.description) |description| {
owned_description = try arena.dupe(u8, description);
}
const index = self.commands.items.len;
try self.commands.append(self.gpa, Command{
.name = try arena.dupe(u8, opts.name),
.description = owned_description,
});
return @enumFromInt(index);
}
pub fn showUsage(self: *CLI, program_name: []const u8) void {
self.stderr.write("Usage: {s}", .{program_name});
if (self.options.items.len > 0) {
self.stderr.write(" [options]", .{});
}
if (self.commands.items.len > 0) {
self.stderr.write(" <command>", .{});
}
self.stderr.write("\n", .{});
if (self.commands.items.len > 0) {
self.stderr.write("\n", .{});
self.stderr.write("Commands:\n", .{});
for (self.commands.items) |command| {
self.stderr.write(" {s} {s}\n", .{command.name, command.description});
}
}
if (self.options.items.len > 0) {
self.stderr.write("\n", .{});
self.stderr.write("Options:\n", .{});
for (0.., self.options.items) |i, option| {
if (i > 0) {
self.stderr.write("\n", .{});
}
self.stderr.write(" --{s}", .{option.long_name});
if (option.kind == .single_argument) {
// TODO: Make this hint renameable
self.stderr.write(" <value>", .{});
}
self.stderr.write("\n", .{});
if (option.description.len > 0) {
self.stderr.write(" {s}\n", .{option.description});
}
}
}
self.stderr.flush();
}
pub const ParseResult = struct {
arena: std.heap.ArenaAllocator,
command: ?Command.Id,
options: []?Option.Value,
pub fn init(gpa: std.mem.Allocator, options_len: usize) !ParseResult {
var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit();
const options = try arena.allocator().alloc(?Option.Value, options_len);
@memset(options, null);
return ParseResult{
.arena = arena,
.command = null,
.options = options
};
}
pub fn deinit(self: *ParseResult) void {
self.arena.deinit();
}
pub fn getOption(self: ParseResult, id: Option.Id) ?Option.Value {
return self.options[@intFromEnum(id)];
}
pub fn isSet(self: ParseResult, id: Option.Id) bool {
return self.getOption(id) != null;
}
// TODO: I don't like this function name
pub fn getOptionArgument(self: ParseResult, id: Option.Id) ?[]const u8 {
if (self.getOption(id)) |option| {
return option.argument;
}
return null;
}
};
const Parser = struct {
args: []const []const u8,
cursor: u32,
pub fn peek(self: *Parser) ?[]const u8 {
if (self.cursor < self.args.len) {
return self.args[self.cursor];
}
return null;
}
pub fn take(self: *Parser) ?[]const u8 {
if (self.peek()) |value| {
self.cursor += 1;
return value;
}
return null;
}
};
fn printErrorAndExit(self: *CLI, comptime fmt: []const u8, args: anytype) noreturn {
self.stderr.write("ERROR: " ++ fmt ++ "\n", args);
self.stderr.flush();
std.process.exit(1);
}
// TODO: I don't like the fact that this call `std.process.exit()`
// But for now this is the most convenient thing.
pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !ParseResult {
var result = try ParseResult.init(gpa, self.options.items.len);
errdefer result.deinit();
var parser = Parser{
.args = args,
.cursor = 1
};
next_arg: while (parser.take()) |arg| {
if (std.mem.startsWith(u8, arg, "--")) {
const long_option_name = arg[2..];
if (self.getOptionByLongName(long_option_name)) |option_id| {
const option = self.options.items[@intFromEnum(option_id)];
var result_value = Option.Value{};
if (option.kind == .single_argument) {
const argument = parser.take() orelse {
self.printErrorAndExit("Missing required argument for '{s}'", .{arg});
};
result_value.argument = argument;
}
result.options[@intFromEnum(option_id)] = result_value;
continue :next_arg;
}
self.printErrorAndExit("Unknown option '{s}'", .{arg});
}
if (result.command == null) {
for (0.., self.commands.items) |i, command| {
if (std.mem.eql(u8, command.name, arg)) {
result.command = @enumFromInt(i);
continue :next_arg;
}
}
self.printErrorAndExit("Unknown command '{s}'", .{arg});
} else {
self.printErrorAndExit("Unknown argument '{s}'", .{arg});
}
}
return result;
}

View File

@ -35,7 +35,7 @@ const KindDynamic = struct {
.init = try ensureLookup(lib, GameCallbacks.InitFn, "init"), .init = try ensureLookup(lib, GameCallbacks.InitFn, "init"),
.tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"), .tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"),
.deinit = try ensureLookup(lib, GameCallbacks.DeinitFn, "deinit"), .deinit = try ensureLookup(lib, GameCallbacks.DeinitFn, "deinit"),
.loadAssets = if (build_options.load_assets_at_runtime) try ensureLookup(lib, GameCallbacks.LoadAssetsFn, "loadAssets") else {}, .loadAssets = if (build_options.asset_hot_reload) try ensureLookup(lib, GameCallbacks.LoadAssetsFn, "loadAssets") else {},
.debug = if (build_options.has_imgui) try ensureLookup(lib, GameCallbacks.DebugFn, "debug") else {}, .debug = if (build_options.has_imgui) try ensureLookup(lib, GameCallbacks.DebugFn, "debug") else {},
}; };
} }
@ -212,14 +212,14 @@ const KindStatic = struct {
.init = game.init, .init = game.init,
.deinit = game.deinit, .deinit = game.deinit,
.tick = game.tick, .tick = game.tick,
.loadAssets = if (build_options.load_assets_at_runtime) game.loadAssets else {}, .loadAssets = if (build_options.asset_hot_reload) game.loadAssets else {},
.debug = if (build_options.has_imgui) game.debug else {}, .debug = if (build_options.has_imgui) game.debug else {},
}; };
} }
}; };
pub fn initDynamic(gpa: std.mem.Allocator, path: []const u8) !GameLinking { pub fn initDynamic(gpa: std.mem.Allocator, path: []const u8) !GameLinking {
if (!build_options.hot_reload) { if (!build_options.code_hot_reload) {
return error.NotSupported; return error.NotSupported;
} }
@ -246,7 +246,7 @@ pub fn initStatic() !GameLinking {
} }
pub fn deinit(self: *GameLinking, gpa: std.mem.Allocator) void { pub fn deinit(self: *GameLinking, gpa: std.mem.Allocator) void {
if (build_options.hot_reload and self.kind == .dynamic) { if (build_options.code_hot_reload and self.kind == .dynamic) {
self.kind.dynamic.deinit(gpa); self.kind.dynamic.deinit(gpa);
} }
} }
@ -256,7 +256,7 @@ pub fn check(self: *GameLinking) !void {
return; return;
} }
if (!build_options.hot_reload) { if (!build_options.code_hot_reload) {
return; return;
} }

View File

@ -10,6 +10,7 @@ const Input = @import("./input.zig");
const ScreenScalar = @import("./screen_scaler.zig"); const ScreenScalar = @import("./screen_scaler.zig");
pub const imgui = @import("./imgui.zig"); pub const imgui = @import("./imgui.zig");
pub const Graphics = @import("./graphics.zig"); pub const Graphics = @import("./graphics.zig");
pub const CLI = @import("./cli.zig");
const Audio = @import("./audio.zig"); const Audio = @import("./audio.zig");
const tracy = @import("tracy"); const tracy = @import("tracy");
const builtin = @import("builtin"); const builtin = @import("builtin");
@ -300,12 +301,12 @@ const AssetManager = struct {
queued_loads_arena: std.heap.ArenaAllocator, queued_loads_arena: std.heap.ArenaAllocator,
queued_loads: std.ArrayList(LoadAsset), queued_loads: std.ArrayList(LoadAsset),
pub fn init(gpa: std.mem.Allocator, dir: ?std.fs.Dir) AssetManager { pub fn init(gpa: std.mem.Allocator) AssetManager {
return AssetManager{ return AssetManager{
.gpa = gpa, .gpa = gpa,
.assets = .empty, .assets = .empty,
.textures = .empty, .textures = .empty,
.dir = dir, .dir = null,
.queued_loads_arena = .init(gpa), .queued_loads_arena = .init(gpa),
.queued_loads = .empty .queued_loads = .empty
}; };
@ -419,13 +420,20 @@ recording: ?Recording,
play_recording: bool, play_recording: bool,
recording_tick: u64, recording_tick: u64,
const RunOptions = struct { pub const RunOptions = struct {
pub const AssetLoading = union(enum) {
bundle_bytes: []const u8,
bundle_path: []const u8,
dir_path: []const u8
};
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
game_library_path: ?[]const u8 = null, game_library_path: ?[]const u8 = null,
asset_bundle_bytes: ?[]const u8 = null,
do_recording: bool = (builtin.mode == .Debug), do_recording: bool = (builtin.mode == .Debug),
screen_width: u32 = 640, screen_width: u32 = 640,
screen_height: u32 = 480, screen_height: u32 = 480,
asset_loading: AssetLoading
}; };
pub fn run(self: *Engine, opts: RunOptions) !void { pub fn run(self: *Engine, opts: RunOptions) !void {
@ -436,30 +444,11 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
game_linking = try .initStatic(); 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{ const initial = GameState.Init{
.seed = @bitCast(std.time.milliTimestamp()), .seed = @bitCast(std.time.milliTimestamp()),
.screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height) .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{ self.* = Engine{
.allocator = opts.allocator, .allocator = opts.allocator,
.graphics = try .init(.{ .graphics = try .init(.{
@ -484,16 +473,63 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
.play_recording = false, .play_recording = false,
.recording_tick = 0, .recording_tick = 0,
.seed = initial.seed, .seed = initial.seed,
.asset_manager = .init(opts.allocator, asset_dir), .asset_manager = .init(opts.allocator),
.asset_bundle = maybe_asset_bundle, .asset_bundle = null,
}; };
if (build_options.load_assets_at_runtime) { switch (opts.asset_loading) {
const dir = asset_dir.?; .bundle_bytes => |bundle_bytes| {
var asset_bundle = AssetBundle.init();
errdefer asset_bundle.deinit(opts.allocator);
var reader = std.Io.Reader.fixed(bundle_bytes);
asset_bundle.read(opts.allocator, &reader) catch |e| {
log.err("Failed to load asset bundle: {}", .{e});
return error.FailedToLoadAssetBundle;
};
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.asset_bundle = asset_bundle;
},
.dir_path => |asset_dir_path| {
if (!build_options.asset_hot_reload) {
return error.RuntimeAssetLoadingDisabled;
}
const dir = try std.fs.cwd().openDir(asset_dir_path, .{ .iterate = true });
var loader: Lib.Asset.Loader = .init(opts.allocator, dir); var loader: Lib.Asset.Loader = .init(opts.allocator, dir);
defer loader.deinit(); defer loader.deinit();
game_linking.callbacks.loadAssets(&loader); game_linking.callbacks.loadAssets(&loader);
for (loader.assets.items) |asset_desc| { for (loader.assets.items) |asset_desc| {
switch (asset_desc.kind) { switch (asset_desc.kind) {
.font => |path| { .font => |path| {
@ -544,37 +580,13 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
}, },
} }
} }
}
if (maybe_asset_bundle) |asset_bundle| { self.asset_manager.dir = dir;
for (asset_bundle.fonts.items) |font| { },
const font_id = try self.graphics.addFont(); .bundle_path => {
try self.asset_manager.assets.append(self.allocator, .{ // TODO:
.id = font.asset_id, unreachable;
.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()); self.game_state.runGameInit(game_linking.callbacks.init, initial, self.asset_manager.initRegistry());
@ -852,7 +864,7 @@ fn showDebugWindow(self: *Engine) !void {
imgui.textFmt("Linking: static", .{ }); imgui.textFmt("Linking: static", .{ });
} }
if (build_options.load_assets_at_runtime) { if (build_options.asset_hot_reload) {
imgui.textFmt("Asset loading: runtime (from {s})", .{build_options.asset_dir}); imgui.textFmt("Asset loading: runtime (from {s})", .{build_options.asset_dir});
} else { } else {
imgui.textFmt("Asset loading: builtin", .{}); imgui.textFmt("Asset loading: builtin", .{});

View File

@ -493,7 +493,7 @@ pub const Callbacks = struct {
init: InitFn, init: InitFn,
tick: TickFn, tick: TickFn,
deinit: DeinitFn, deinit: DeinitFn,
loadAssets: if (build_options.load_assets_at_runtime) LoadAssetsFn else void, loadAssets: if (build_options.asset_hot_reload) LoadAssetsFn else void,
debug: if (build_options.has_imgui) DebugFn else void debug: if (build_options.has_imgui) DebugFn else void
}; };
@ -512,7 +512,7 @@ pub fn exportCallbacksIfNeeded(
@export(init, .{ .name = "init", .linkage = .strong }); @export(init, .{ .name = "init", .linkage = .strong });
@export(tick, .{ .name = "tick", .linkage = .strong }); @export(tick, .{ .name = "tick", .linkage = .strong });
@export(deinit, .{ .name = "deinit", .linkage = .strong }); @export(deinit, .{ .name = "deinit", .linkage = .strong });
if (build_options.load_assets_at_runtime) { if (build_options.asset_hot_reload) {
@export(assets, .{ .name = "loadAssets", .linkage = .strong }); @export(assets, .{ .name = "loadAssets", .linkage = .strong });
} }
if (build_options.has_imgui) { if (build_options.has_imgui) {

View File

@ -1,18 +1,20 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Engine = @import("engine"); const Engine = @import("engine");
const CLI = Engine.CLI;
const build_options = Engine.Lib.build_options; const build_options = Engine.Lib.build_options;
pub const std_options: std.Options = .{
// TODO: Override .logFn for logging to a file
};
var engine: Engine = undefined; var engine: Engine = undefined;
pub fn main() !void { pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init; var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit(); defer _ = debug_allocator.deinit();
var asset_bundle_bytes: ?[]const u8 = null; const isWasm = builtin.cpu.arch.isWasm();
if (!build_options.load_assets_at_runtime) {
asset_bundle_bytes = @embedFile("asset_bundle");
}
// TODO: Use tracy TracingAllocator // TODO: Use tracy TracingAllocator
var allocator: std.mem.Allocator = undefined; var allocator: std.mem.Allocator = undefined;
@ -24,18 +26,84 @@ pub fn main() !void {
allocator = std.heap.smp_allocator; allocator = std.heap.smp_allocator;
} }
var game_library_path: ?[]const u8 = null;
defer if (game_library_path) |str| allocator.free(str);
var asset_loading: Engine.RunOptions.AssetLoading = undefined;
if (!build_options.asset_hot_reload) {
asset_loading = .{
.bundle_bytes = @embedFile("asset_bundle")
};
} else {
asset_loading = .{
.dir_path = build_options.asset_dir
};
}
if (!isWasm) {
var cli: CLI = undefined;
cli.init(allocator);
defer cli.deinit();
const opt_help = try cli.addOption(.{
.long_name = "help",
.description = "Show this message"
});
var opt_game_lib_path: ?CLI.Option.Id = null;
if (build_options.code_hot_reload) {
opt_game_lib_path = try cli.addOption(.{
.long_name = "game-lib-path",
.description = "Load game code from dynamic library",
.kind = .single_argument
});
}
const cmd_version = try cli.addCommand(.{
.name = "version",
.description = "Show semantic version and build options"
});
const cmd_launch = try cli.addCommand(.{
.name = "launch",
});
const args = try std.process.argsAlloc(allocator); const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args); defer std.process.argsFree(allocator, args);
var game_library_path: ?[]const u8 = null; var parsed = try cli.parse(allocator, args);
if (args.len >= 2) { defer parsed.deinit();
game_library_path = args[1];
if (parsed.isSet(opt_help)) {
cli.showUsage(args[0]);
std.process.exit(0);
}
if (opt_game_lib_path) |opt| {
if (parsed.getOptionArgument(opt)) |arg| {
game_library_path = try allocator.dupe(u8, arg);
}
}
const command = parsed.command orelse cmd_launch;
if (command == cmd_version) {
cli.stdout.write("Version: {s}\n", .{ "TODO:" });
cli.stdout.flush();
std.process.exit(0);
} else if (command == cmd_launch) {
// Nothing to be done
} else {
unreachable;
}
} }
try engine.run(.{ try engine.run(.{
.allocator = allocator, .allocator = allocator,
.game_library_path = game_library_path, .game_library_path = game_library_path,
.asset_bundle_bytes = asset_bundle_bytes .asset_loading = asset_loading
}); });
} }

View File

@ -51,13 +51,18 @@ pub fn main() !void {
const font_data = try readFileAll(allocator, asset_dir, path); const font_data = try readFileAll(allocator, asset_dir, path);
defer allocator.free(font_data); defer allocator.free(font_data);
try bundle.appendFont(allocator, id, font_data); // TODO:
try bundle.appendFont(allocator, id, font_data, .{ .format = .ttf });
}, },
.sound => |path| { .sound => |path| {
const sound_data = try readFileAll(allocator, asset_dir, path); const sound_data = try readFileAll(allocator, asset_dir, path);
defer allocator.free(sound_data); defer allocator.free(sound_data);
try bundle.appendSound(allocator, id, sound_data); try bundle.appendSound(allocator, id, sound_data, .{
// TODO:
.format = .vorbis,
.playback_style = .decode_once
});
}, },
.texture => |opts| { .texture => |opts| {
const texture_data = try readFileAll(allocator, asset_dir, opts.path); const texture_data = try readFileAll(allocator, asset_dir, opts.path);