pub var NeonObjectTable = core.EngineObjectVTable.from(@This(), "main"); const GameContext = @This(); allocator: std.mem.Allocator, pathBuffer: [8192]u8, projectNameBuffer: [8192]u8, commandQueue: core.RingQueue(Task), activeCommand: ?*sys.SubprocessTask = null, targetName: ?[]u8 = null, targetDir: ?[]u8 = null, engineRelative: ?[]u8 = null, fingerprint: ?[]const u8 = null, showInstructions: bool = false, const Task = union(enum(u8)) { subprocess: *sys.SubprocessTask, func: *const fn (*GameContext) void, }; pub fn init(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = .{ .allocator = allocator, .pathBuffer = std.mem.zeroes([8192]u8), .projectNameBuffer = std.mem.zeroes([8192]u8), .commandQueue = try core.RingQueue(Task).init(self.allocator, 4096), .targetName = try allocator.dupe(u8, "sample"), .engineRelative = try allocator.dupe(u8, "Backlog"), }; std.mem.copyForwards(u8, &self.projectNameBuffer, "newProject"); return self; } pub fn prepare(self: *@This()) !void { _ = self; core.engine_log("program ready", .{}); } pub fn tick(self: *@This(), dt: f64) void { _ = dt; self.tickTasks() catch {}; if (self.activeCommand != null or self.commandQueue.count() > 0) { ig.setNextWindowPos(.{ .x = 20, .y = 20 }, .{}, .{}); if (ig.begin("tasks", null, .{ .always_auto_resize = true, .no_title_bar = true, .no_move = true, .no_resize = true, .no_collapse = true, })) { ig.textf("outstanding tasks: {d}", .{self.commandQueue.count() + 1}); if (self.activeCommand) |cmd| { ig.textf("current command: ", .{}); for (cmd.argsOwned.items) |arg| { ig.textf("{s}", .{arg}); ig.sameLine(0, 4); } } } ig.end(); return; } ig.setNextWindowPos(.{ .x = 200, .y = 200 }, .{}, .{}); if (ig.begin("New Project...", null, .{ .always_auto_resize = true, .no_move = true, .no_resize = true, .no_collapse = true, })) { if (ig.inputText( "##ProjectName", &self.projectNameBuffer, self.projectNameBuffer.len, .{}, null, null, )) { // core.engine_log("huh", .{}); } ig.setNextItemWidth(600); if (ig.inputText( "##Path", &self.pathBuffer, self.pathBuffer.len, .{}, null, null, )) { // core.engine_log("huh", .{}); } ig.sameLine(0, 3); if (ig.button("...", .{})) { core.engine_log("opening NFD", .{}); platform.file_dialogue.asyncOpenFolder(.{ .callback = onFolderSelected, .callbackContext = self, }) catch @panic(" unable to open dialog"); } if (ig.button("Create", .{})) { const asSlice = std.mem.span(@as([*c]const u8, @ptrCast(&self.pathBuffer))); core.engine_log("creating project: {s}", .{self.pathBuffer}); self.createProject(asSlice) catch {}; } } ig.end(); if (self.showInstructions) { ig.setNextWindowPos(.{ .x = 100, .y = 100 }, .{}, .{}); ig.setNextWindowSize(.{ .x = 800, .y = 500 }, .{}); if (ig.begin("Instructions", null, .{ .always_auto_resize = true, .no_move = true, .no_resize = true, .no_collapse = true, })) { ig.textf("project generated and git repo cloned, run zig build install and you might need to update the fingerprint", .{}); if (ig.button("close..", .{})) { self.showInstructions = false; } } ig.end(); } } fn tickTasks(self: *@This()) !void { if (self.activeCommand == null) { if (self.commandQueue.popFromLocked()) |task| { switch (task) { .subprocess => |t| { self.activeCommand = t; // core.engine_log("starting task {any}", .{t.args}); try t.run(); }, .func => |f| { f(self); }, } } } if (self.activeCommand) |active| { if (active.checkComplete()) { active.destroy(); core.engine_logs("task complete"); self.activeCommand = null; } } } pub fn createProject(self: *@This(), targetDir: []const u8) !void { self.targetDir = try self.allocator.dupe(u8, targetDir); { const asSlice = std.mem.span(@as([*c]const u8, @ptrCast(&self.projectNameBuffer))); self.targetName = try self.allocator.dupe(u8, asSlice); } try self.commandQueue.pushLocked(.{ .subprocess = try sys.runCommand( self.allocator, &.{ "zig", "init" }, targetDir, ) }); // const deployShaderDir = try std.fmt.allocPrint(self.allocator, "{s}/content/_shaders", .{self.targetDir.?}); if (builtin.os.tag == .windows) { // os copy over the entire directory } else {} try self.commandQueue.pushLocked(.{ .func = updateBuildZig }); try self.commandQueue.pushLocked(.{ .subprocess = try sys.runCommand( self.allocator, &.{ "git", "clone", "git@github.com:peterino2/BacklogEngine.git", "Backlog" }, targetDir, ) }); try self.commandQueue.pushLocked(.{ .func = copyShaders }); try self.commandQueue.pushLocked(.{ .func = displayInstructions }); } fn copyShaders(self: *@This()) void { self.copyShadersWrap() catch {}; } fn copyShadersWrap(self: *@This()) !void { var srcDir = try std.fs.cwd().openDir("content/_shaders", .{ .iterate = true }); defer srcDir.close(); var targetDir = try std.fs.cwd().openDir(self.targetDir.?, .{}); defer targetDir.close(); try targetDir.makeDir("content"); try targetDir.makeDir("content/_shaders"); var dstDir = try targetDir.openDir("content/_shaders", .{}); defer dstDir.close(); try copyRecursiveDir(self.allocator, &srcDir, &dstDir); } fn loadFileAllocFromCwd(allocator: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) ![]u8 { var file = try dir.openFile(path, .{}); defer file.close(); const filesize = (try file.stat()).size + 1; // add null byte const buffer: []u8 = try allocator.alloc(u8, filesize); errdefer allocator.free(buffer); _ = try file.read(buffer[0 .. buffer.len - 1]); buffer[buffer.len - 1] = 0; return buffer; } fn displayInstructions(self: *@This()) void { self.showInstructions = true; } fn updateBuildZig(self: *@This()) void { core.engine_logs("updating build.zig.zon"); var workingDir = std.fs.cwd().openDir(self.targetDir.?, .{}) catch return; defer workingDir.close(); const buildZigZonPath = std.fmt.allocPrint(self.allocator, "{s}/build.zig.zon", .{self.targetDir.?}) catch return; defer self.allocator.free(buildZigZonPath); const buildZigContents = loadFileAllocFromCwd(self.allocator, workingDir, buildZigZonPath) catch return; defer self.allocator.free(buildZigContents); core.engine_log("{s}", .{buildZigContents}); // walk forward until we find the fingerprint value var window: []const u8 = buildZigContents[0..]; var offset: usize = 0; while (offset < buildZigContents.len) : (offset += 1) { window = buildZigContents[offset..buildZigContents.len]; if (std.mem.startsWith(u8, window, ".fingerprint")) { // walk forward until i find the comma var windowEnd: usize = offset; while (windowEnd < buildZigContents.len) : (windowEnd += 1) { if (buildZigContents[windowEnd] == ',') { self.updateFingerPrint(buildZigContents[offset..windowEnd]); self.writeBuildZigFiles() catch {}; return; } } } } core.engine_logs("unable to update window"); } fn writeBuildZigFiles(self: *@This()) !void { // copy over the entire templates directory. // 1. make a content folder // 2. create src/main.zig // 3. build.zig // 4. build.zig.zon var workingDir = try std.fs.cwd().openDir(self.targetDir.?, .{ .iterate = true }); defer workingDir.close(); core.engine_logs("deleting files..."); try workingDir.deleteFile("src/root.zig"); try workingDir.deleteFile("src/main.zig"); try workingDir.deleteFile("build.zig"); try workingDir.deleteFile("build.zig.zon"); // 1. write src/main.zig try self.writeMainZig(&workingDir); try self.writeBuildZig(&workingDir); try self.writeBuildZigZon(&workingDir); } fn writeBuildZig(self: *@This(), workingDir: *std.fs.Dir) !void { const out_path = "build.zig"; var content = std.ArrayList(u8){}; defer content.deinit(self.allocator); const template = \\ const std = @import("std"); \\ const Backlog = @import("Backlog"); \\ \\ pub fn build(b: *std.Build) void {{ \\ const target = b.standardTargetOptions(.{{}}); \\ const optimize = b.standardOptimizeOption(.{{}}); \\ \\ var blbuild = Backlog.init(b, .{{ \\ .target = target, \\ .optimize = optimize, \\ .backlogRoot = "{s}", \\ }}); ; try content.print(self.allocator, template, .{self.engineRelative.?}); const template2 = \\ const program = blbuild.program(.{{ \\ .name = "{s}", \\ .desc = "simple program built with backlog", \\ .root_source_file = b.path("src/main.zig"), \\ }}); \\ \\ program.setModuleEnabled("imgui", true); \\ \\ _ = program.compileInstall(); \\ }} ; try content.print(self.allocator, template2, .{self.targetName.?}); try content.append(self.allocator, 0); try self.writeZigFile(workingDir, out_path, content.items); } const entries: []const struct { name: []const u8, path: []const u8 } = &.{ .{ .name = "SpirvReflect", .path = "lib/spirv-reflect-zig" }, .{ .name = "zphysics", .path = "lib/zphysics" }, .{ .name = "spng", .path = "lib/spng" }, .{ .name = "ozz", .path = "lib/ozz" }, .{ .name = "sdl3", .path = "lib/sdl3" }, .{ .name = "miniaudio", .path = "lib/miniaudio" }, .{ .name = "lua", .path = "lib/lua" }, }; fn writeBuildZigZon(self: *@This(), workingDir: *std.fs.Dir) !void { var content = std.ArrayList(u8){}; defer content.deinit(self.allocator); try content.print(self.allocator, ".{{\n", .{}); try content.print(self.allocator, \\ .name = .{s}, \\ .version = "0.0.0", \\ .dependencies = .{{ \\ .Backlog = .{{ .path = "{s}" }}, , .{ std.fs.path.basename(self.targetDir.?), self.engineRelative.? }); for (entries) |entry| { try content.print(self.allocator, " .{s} = .{{ .path = \"{s}/{s}\" }},\n", .{ entry.name, self.engineRelative.?, entry.path }); } try content.print(self.allocator, \\ }}, \\ , .{}); try content.print(self.allocator, \\ .paths = .{{ "" }}, \\ , .{}); try content.print(self.allocator, \\ .fingerprint = {s}, \\ , .{self.fingerprint.?}); try content.print(self.allocator, "}}", .{}); const file = try workingDir.createFile("build.zig.zon", .{}); defer file.close(); var writer = file.writer(&wbuf); try writer.interface.writeAll(content.items); } fn writeMainZig(self: *@This(), workingDir: *std.fs.Dir) !void { var content = std.ArrayList(u8){}; defer content.deinit(self.allocator); const template = \\ pub var NeonObjectTable = core.EngineObjectVTable.from(@This(), "main"); \\ \\ allocator: std.mem.Allocator, \\ \\ pub fn init(allocator: std.mem.Allocator) !*@This() { \\ const self = try allocator.create(@This()); \\ self.* = .{ \\ .allocator = allocator, \\ }; \\ return self; \\ } \\ \\ pub fn prepare(self: *@This()) !void { \\ _ = self; \\ core.engine_log("program ready", .{}); \\ } \\ \\ pub fn tick(self: *@This(), dt: f64) void { \\ _ = self; \\ _ = dt; \\ ig.showDemoWindow(null); \\ } \\ \\ pub fn deinit(self: *@This()) void { \\ self.allocator.destroy(self); \\ } \\ \\ pub fn main() anyerror!void { \\ var spec = try api.getSpec(" ; const template2 = \\"); \\ _ = api.startEngine(&NeonObjectTable, &spec); \\ } \\ \\ const std = @import("std"); \\ const api = @import("backlog"); \\ const ig = api.imgui.api; \\ const core = api.core; \\ ; const allocator = self.allocator; try content.appendSlice(allocator, template); try content.appendSlice(allocator, self.targetName.?); try content.appendSlice(allocator, template2); try content.append(allocator, 0); const out_path = "src/main.zig"; std.debug.print("{s}", .{content.items}); try self.writeZigFile(workingDir, out_path, content.items); } var wbuf: [4096]u8 = undefined; fn writeZigFile(self: *@This(), workingDir: *std.fs.Dir, out_path: []const u8, contents: []const u8) !void { var ast = try std.zig.Ast.parse(self.allocator, @as([:0]const u8, @ptrCast(contents[0 .. contents.len - 1])), .zig); defer ast.deinit(self.allocator); const out = try ast.renderAlloc(self.allocator); const file = try workingDir.createFile(out_path, .{}); defer file.close(); var fw = file.writer(&wbuf); try fw.interface.writeAll(out); } fn updateFingerPrint(self: *@This(), zonEntry: []const u8) void { // find the first number var left: usize = 0; while (zonEntry[left] != '0') : (left += 1) {} core.engine_log(">> FINGERPRINT '{s}'", .{zonEntry[left..zonEntry.len]}); self.fingerprint = zonEntry[left..zonEntry.len]; } pub fn onFolderSelected( ctx: ?*anyopaque, path: ?[]const u8, ) void { const self: *@This() = @ptrCast(@alignCast(ctx)); if (path) |p| { std.mem.copyForwards(u8, &self.pathBuffer, p); self.pathBuffer[p.len] = 0; } } pub fn deinit(self: *@This()) void { if (self.targetDir) |t| { self.allocator.free(t); } self.allocator.destroy(self); } fn copyRecursiveDir( allocator: std.mem.Allocator, src_dir: *std.fs.Dir, dest_dir: *std.fs.Dir, ) !void { var walker = try src_dir.walk(allocator); defer walker.deinit(); while (try walker.next()) |entry| { switch (entry.kind) { .file => { try entry.dir.copyFile(entry.basename, dest_dir.*, entry.path, .{}); }, .directory => { try dest_dir.makeDir(entry.path); }, else => {}, } } } pub fn main() anyerror!void { var spec = try api.getSpec("New Project Dialogue"); try spec.put("useGPA", .{ .boolean = false }); _ = api.startEngine(&NeonObjectTable, &spec); } const builtin = @import("builtin"); const std = @import("std"); const api = @import("backlog"); const ig = api.imgui.api; const platform = api.platform; const core = api.core; const sys = api.sys;