pub const ecs = @import("ecs"); pub const rendering = @import("rendering"); pub const mods = @import("mods"); const Renderer = rendering.Renderer; const api = @cImport({ @cInclude("sideros_api.h"); }); const std = @import("std"); const systems = @import("systems.zig"); var gpa: std.heap.DebugAllocator(.{}) = .{}; const allocator = gpa.allocator(); var pool: ecs.Pool = undefined; var renderer: Renderer = undefined; var camera: rendering.Camera = .{ .position = .{ 0.0, 5.0, -5.0 }, }; var input: ecs.Input = .{ .key_pressed = .{false} ** @intFromEnum(ecs.Input.KeyCode.menu) }; var resources: ecs.Resources = undefined; const ModInfo = struct { name: []const u8, runtime: *mods.Runtime, modIdx: u32, }; var loadedMods: std.ArrayListUnmanaged(ModInfo) = .empty; fn openOrCreateDir(fs: std.fs.Dir, path: []const u8) !std.fs.Dir { var dir: std.fs.Dir = undefined; dir = fs.openDir(path, .{ .iterate = true }) catch |err| { if (err == std.fs.Dir.OpenError.FileNotFound) { try fs.makeDir(path); dir = try fs.openDir(path, .{ .iterate = true }); return dir; } else { return err; } }; return dir; } fn untarToDirAndGetFile(fs: std.fs.Dir, name: []const u8, unpack: []const u8) !std.fs.File { var buffer: [1024]u8 = undefined; var modDir = try openOrCreateDir(fs, unpack); defer modDir.close(); var tarFile = try fs.openFile(try std.fmt.bufPrint(&buffer, "{s}.tar", .{name}), .{}); defer tarFile.close(); const tarData = try tarFile.readToEndAlloc(allocator, 1_000_000); defer allocator.free(tarData); var tarReader = std.io.Reader.fixed(tarData); try std.tar.pipeToFileSystem(modDir, &tarReader, .{}); return try fs.openFile(try std.fmt.bufPrint(&buffer, "{s}/main.wasm", .{unpack}), .{}); } fn loadMod(entry: std.fs.Dir.Entry) !void { const modName = entry.name.ptr[0 .. entry.name.len - 4]; const fullDir = std.fmt.allocPrint(allocator, "assets/mods/{s}", .{modName}) catch @panic("Failed to allocate for fullDir"); defer allocator.free(fullDir); const modDir = try std.fmt.allocPrint(allocator, "{s}_siderosmod__", .{fullDir}); const global_runtime = allocator.create(mods.GlobalRuntime) catch @panic("Failed to create global runtime"); global_runtime.* = mods.GlobalRuntime.init(allocator); std.fs.cwd().deleteTree(modDir) catch |err| { std.debug.panic("Failed to delete {s} (reason: {any})", .{ modDir, err }); }; var file = untarToDirAndGetFile(std.fs.cwd(), fullDir, modDir) catch |err| { return err; }; defer std.fs.cwd().deleteTree(modDir) catch |err| { std.debug.panic("Failed to delete {s} (reason: {any})", .{ modDir, err }); }; defer file.close(); // TODO(luccie): Make this be able to construct a buffer for the whole file const buffer = try allocator.alloc(u8, 1_000_000); var reader = file.reader(buffer); var parser = mods.Parser.init(allocator, &reader.interface) catch @panic("Failed to init parser"); defer parser.deinit(); parser.parseModule() catch |err| { // TODO(luccie): Find a better option for the expression `parser.reader.buffer[parser.reader.seek]` std.debug.print("[ERROR]: error {any} at byte {x}(0x{x})\n", .{ err, parser.reader.seek, parser.reader.buffer[parser.reader.seek] }); return err; }; const module = parser.module(); for (0..parser.globalTypes.len) |i| { global_runtime.addGlobal(@intCast(i), parser.globalTypes[i], parser.globalValues[i]) catch @panic("Failed to add runtime global"); } var runtime = allocator.create(mods.Runtime) catch |err| { std.debug.print("Failed to create runtime", .{}); return err; }; runtime.* = mods.Runtime.init(allocator, module, global_runtime) catch |err| { std.debug.print("Failed to init runtime", .{}); return err; }; const modIdx: u32 = @intCast(loadedMods.items.len); var parameters = [_]mods.VM.Value{.{ .i32 = @intCast(modIdx) }}; runtime.externalCall(allocator, .init, ¶meters) catch @panic("Failed to call to init"); const result = runtime.stack.pop().?.i64; if (result != 0) { std.debug.print("[ERROR]: Mod {s} init returned {d}\n", .{ modName, result }); return error.Failure; } loadedMods.append(allocator, .{ .name = try allocator.dupe(u8, modName), .runtime = runtime, .modIdx = modIdx }) catch @panic("Failed to append to loadedMods"); } fn init_mods() void { var modsDir = std.fs.cwd().openDir("./assets/mods", .{ .iterate = true }) catch @panic("Failed to open assets/mods"); defer modsDir.close(); var modsDirIter = modsDir.iterate(); while (modsDirIter.next() catch @panic("Failed to get next iteration of mods directory")) |entry| { if (std.mem.indexOf(u8, entry.name, "siderosmod") != null) { std.fs.cwd().deleteTree(entry.name) catch |err| { std.debug.panic("Failed to delete {s} (reason: {any})", .{ entry.name, err }); }; continue; } if (entry.kind != std.fs.File.Kind.file) { std.debug.panic("TODO: Search recursively for mods\n", .{}); } const extension = entry.name.ptr[entry.name.len - 4 .. entry.name.len]; if (!std.mem.eql(u8, extension, ".tar")) { std.debug.print("[WARNING]: Found non tar extension in mods directory\n", .{}); continue; } loadMod(entry) catch @panic("Failed to load mod"); } } fn sideros_init(io: std.Io, init: api.GameInit) void { resources = .{ .camera = &camera, .renderer = undefined, .input = &input, .terrain = undefined, }; ecs.hooks.init(allocator) catch @panic("TODO: handle this"); ecs.hooks.addHook(.scroll, systems.zoomCamera) catch @panic("TODO handle this"); pool = ecs.Pool.init(allocator, &resources) catch @panic("TODO: Gracefully handle error"); // TODO(ernesto): I think this @ptrCast are unavoidable but maybe not? renderer = Renderer.init(allocator, io, @ptrCast(init.instance), @ptrCast(init.surface)) catch |err| std.debug.panic("TODO: Gracefully handle error: {}\n", .{err}); resources.terrain = rendering.Terrain.init(allocator, renderer.device, .{ .octaves = 8, .lacunarity = 2.0, .gain = 0.6, .scale = 30, .multiplier = 1.0, .exponent = 1.0, .width = 200, .height = 200, .seed = 2497852058242342, .resolution = 1, }) catch @panic("TODO: handle this"); renderer.terrain_pipeline.setMaps(renderer.device, resources.terrain.texture) catch @panic("TODO: handle this"); pool.addSystemGroup(&[_]ecs.System{ systems.render, systems.moveCamera }, true) catch @panic("TODO: Gracefuly handle error"); pool.resources.renderer = &renderer; pool.tick(); init_mods(); } export fn sideros_update(gameUpdate: api.GameUpdate) callconv(.c) void { _ = gameUpdate; pool.tick(); } export fn sideros_cleanup() callconv(.c) void { for (loadedMods.items) |info| { var runtime = info.runtime; runtime.externalCall(allocator, .deinit, &.{}) catch @panic("Failed to call deinit"); const result = runtime.stack.pop().?.i64; if (result != 0) { std.debug.panic("[ERROR]: Mod {s} deinit returned {d}\n", .{ info.name, result }); } defer runtime.deinit(allocator); defer allocator.free(info.name); } loadedMods.deinit(allocator); renderer.deinit(); pool.deinit(); if (gpa.deinit() != .ok) @panic("Memory leaked"); } export fn sideros_key_callback(key: u32, release: bool) callconv(.c) void { if (key <= @intFromEnum(ecs.Input.KeyCode.menu) and key >= @intFromEnum(ecs.Input.KeyCode.space)) { if (release) { input.key_pressed[key] = false; } else { input.key_pressed[key] = true; } } } export fn sideros_scroll_callback(up: bool) callconv(.c) void { for (ecs.hooks.scroll.items) |hook| { hook(&pool, if (up) .up else .down) catch @panic("TODO: actually handle this"); } }