add cli argument parsing
This commit is contained in:
parent
e995b55ccc
commit
b901c474f0
@ -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/"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
300
engine/src/engine/cli.zig
Normal 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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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", .{});
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user