diff --git a/build.zig b/build.zig index a1b1854..e0df537 100644 --- a/build.zig +++ b/build.zig @@ -21,7 +21,8 @@ pub fn build(b: *std.Build) !void { .has_tracy = b.option(bool, "tracy", "Tracy integration"), .win32_has_console = b.option(bool, "console", "Show console (Window only)"), .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/"), }); diff --git a/engine/build.zig b/engine/build.zig index c5287af..4032015 100644 --- a/engine/build.zig +++ b/engine/build.zig @@ -15,12 +15,12 @@ const InitOptions = struct { dep_engine: *std.Build.Dependency, statically_linked: ?bool = null, - hot_reload: ?bool = null, + code_hot_reload: ?bool = null, has_imgui: ?bool = null, has_tracy: ?bool = null, win32_has_console: ?bool = null, win32_png_icon: ?std.Build.LazyPath = null, - load_assets_at_runtime: ?bool = null, + asset_hot_reload: ?bool = null, asset_dir: std.Build.LazyPath, }; @@ -31,12 +31,12 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { const isDebug = (opts.optimize == .Debug); var has_tracy = false; - var hot_reload = false; - var load_assets_at_runtime = false; + var code_hot_reload = false; + var asset_hot_reload = false; 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; - 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; @@ -45,10 +45,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { var build_options = b.addOptions(); build_options.addOption(bool, "has_imgui", has_imgui); 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, "load_assets_at_runtime", load_assets_at_runtime); - if (load_assets_at_runtime) { + build_options.addOption(bool, "asset_hot_reload", asset_hot_reload); + if (asset_hot_reload) { 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); } + // 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( b, b.graph.host, .ReleaseSafe, engine_module.root_module, @@ -89,7 +91,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { .optimize = opts.optimize, }); runtime_mod.addImport("engine", engine_module.root_module); - if (!load_assets_at_runtime) { + if (!asset_hot_reload) { // TODO: Add compression on asset bundle runtime_mod.addAnonymousImport("asset_bundle", .{ .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| { run_cmd.addArgs(args); } - if (hot_reload) { + if (code_hot_reload) { opts.root_module.resolved_target = opts.target; opts.root_module.optimize = opts.optimize; 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, .{}); + run_cmd.addArg("--game-lib-path"); run_cmd.addArg(b.getInstallPath(.lib, game_lib.out_lib_filename)); run_cmd.step.dependOn(&install_game_lib.step); diff --git a/engine/src/engine/cli.zig b/engine/src/engine/cli.zig new file mode 100644 index 0000000..09cb10d --- /dev/null +++ b/engine/src/engine/cli.zig @@ -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(" ", .{}); + } + 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(" ", .{}); + } + 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; +} diff --git a/engine/src/engine/game_linking.zig b/engine/src/engine/game_linking.zig index 82b118f..da3e224 100644 --- a/engine/src/engine/game_linking.zig +++ b/engine/src/engine/game_linking.zig @@ -35,7 +35,7 @@ const KindDynamic = struct { .init = try ensureLookup(lib, GameCallbacks.InitFn, "init"), .tick = try ensureLookup(lib, GameCallbacks.TickFn, "tick"), .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 {}, }; } @@ -212,14 +212,14 @@ const KindStatic = struct { .init = game.init, .deinit = game.deinit, .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 {}, }; } }; 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; } @@ -246,7 +246,7 @@ pub fn initStatic() !GameLinking { } 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); } } @@ -256,7 +256,7 @@ pub fn check(self: *GameLinking) !void { return; } - if (!build_options.hot_reload) { + if (!build_options.code_hot_reload) { return; } diff --git a/engine/src/engine/root.zig b/engine/src/engine/root.zig index f6ae6d4..4c5c6b5 100644 --- a/engine/src/engine/root.zig +++ b/engine/src/engine/root.zig @@ -10,6 +10,7 @@ const Input = @import("./input.zig"); const ScreenScalar = @import("./screen_scaler.zig"); pub const imgui = @import("./imgui.zig"); pub const Graphics = @import("./graphics.zig"); +pub const CLI = @import("./cli.zig"); const Audio = @import("./audio.zig"); const tracy = @import("tracy"); const builtin = @import("builtin"); @@ -300,12 +301,12 @@ const AssetManager = struct { queued_loads_arena: std.heap.ArenaAllocator, 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{ .gpa = gpa, .assets = .empty, .textures = .empty, - .dir = dir, + .dir = null, .queued_loads_arena = .init(gpa), .queued_loads = .empty }; @@ -419,13 +420,20 @@ recording: ?Recording, play_recording: bool, 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, 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, + + asset_loading: AssetLoading }; pub fn run(self: *Engine, opts: RunOptions) !void { @@ -436,30 +444,11 @@ 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(.{ @@ -484,97 +473,120 @@ pub fn run(self: *Engine, opts: RunOptions) !void { .play_recording = false, .recording_tick = 0, .seed = initial.seed, - .asset_manager = .init(opts.allocator, asset_dir), - .asset_bundle = maybe_asset_bundle, + .asset_manager = .init(opts.allocator), + .asset_bundle = null, }; - if (build_options.load_assets_at_runtime) { - const dir = asset_dir.?; + switch (opts.asset_loading) { + .bundle_bytes => |bundle_bytes| { + var asset_bundle = AssetBundle.init(); + errdefer asset_bundle.deinit(opts.allocator); - 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 font_id = try self.graphics.addFont(); - try self.asset_manager.assets.append(self.allocator, .{ - .id = asset_desc.id, - .kind = .{ .font = font_id } - }); + 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; + }; - try self.asset_manager.enqueueLoad(.{ - .font = .{ - .id = font_id, - .path = path - } - }); - }, - .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 sound_id = try self.audio.addSound(); - 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 - } - }); - }, + 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 } + }); } - } - } - 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 } + }); - 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 + }); + } - 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 } + }); + } - 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); + defer loader.deinit(); + game_linking.callbacks.loadAssets(&loader); + + for (loader.assets.items) |asset_desc| { + switch (asset_desc.kind) { + .font => |path| { + const font_id = try self.graphics.addFont(); + 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 => |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 sound_id = try self.audio.addSound(); + 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.asset_manager.dir = dir; + }, + .bundle_path => { + // TODO: + unreachable; + }, } 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", .{ }); } - if (build_options.load_assets_at_runtime) { + if (build_options.asset_hot_reload) { imgui.textFmt("Asset loading: runtime (from {s})", .{build_options.asset_dir}); } else { imgui.textFmt("Asset loading: builtin", .{}); diff --git a/engine/src/game_lib/root.zig b/engine/src/game_lib/root.zig index 875486a..f54a2ff 100644 --- a/engine/src/game_lib/root.zig +++ b/engine/src/game_lib/root.zig @@ -493,7 +493,7 @@ pub const Callbacks = struct { init: InitFn, tick: TickFn, 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 }; @@ -512,7 +512,7 @@ pub fn exportCallbacksIfNeeded( @export(init, .{ .name = "init", .linkage = .strong }); @export(tick, .{ .name = "tick", .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 }); } if (build_options.has_imgui) { diff --git a/engine/src/runtime/main.zig b/engine/src/runtime/main.zig index 5322095..c8bad01 100644 --- a/engine/src/runtime/main.zig +++ b/engine/src/runtime/main.zig @@ -1,18 +1,20 @@ const std = @import("std"); const builtin = @import("builtin"); const Engine = @import("engine"); +const CLI = Engine.CLI; 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; 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"); - } + const isWasm = builtin.cpu.arch.isWasm(); // TODO: Use tracy TracingAllocator var allocator: std.mem.Allocator = undefined; @@ -24,18 +26,84 @@ pub fn main() !void { allocator = std.heap.smp_allocator; } - const args = try std.process.argsAlloc(allocator); - defer std.process.argsFree(allocator, args); - var game_library_path: ?[]const u8 = null; - if (args.len >= 2) { - game_library_path = args[1]; + 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); + defer std.process.argsFree(allocator, args); + + var parsed = try cli.parse(allocator, args); + defer parsed.deinit(); + + 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(.{ .allocator = allocator, .game_library_path = game_library_path, - .asset_bundle_bytes = asset_bundle_bytes + .asset_loading = asset_loading }); } diff --git a/engine/tools/asset-bundler.zig b/engine/tools/asset-bundler.zig index 820732d..d14c0b3 100644 --- a/engine/tools/asset-bundler.zig +++ b/engine/tools/asset-bundler.zig @@ -51,13 +51,18 @@ pub fn main() !void { const font_data = try readFileAll(allocator, asset_dir, path); defer allocator.free(font_data); - try bundle.appendFont(allocator, id, font_data); + // TODO: + try bundle.appendFont(allocator, id, font_data, .{ .format = .ttf }); }, .sound => |path| { const sound_data = try readFileAll(allocator, asset_dir, path); 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| { const texture_data = try readFileAll(allocator, asset_dir, opts.path);