From 18bff3cc0011396a03f872d0c42dfea3f185088c Mon Sep 17 00:00:00 2001 From: Jeeves Date: Tue, 27 May 2025 22:59:15 -0600 Subject: [PATCH] init (messy) --- .gitattributes | 1 + .gitignore | 16 ++ build.zig | 46 ++++++ build.zig.zon | 17 ++ build.zig.zon2json-lock | 20 +++ flake.lock | 78 +++++++++ flake.nix | 90 ++++++++++ src/c.zig | 4 + src/main.zig | 302 ++++++++++++++++++++++++++++++++++ src/rco.zig | 352 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 926 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 build.zig.zon2json-lock create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/c.zig create mode 100644 src/main.zig create mode 100644 src/rco.zig diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15df977 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Zig-specific build artifacts +.zig-cache/ +zig-cache/ +zig-out/ +/release/ +/debug/ +/build/ +/build-*/ +/docgen_tmp/ + +# Temporary project-specific dirs +vsh/ +dumped/ +menu/ +font/ +reference/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..49acf4d --- /dev/null +++ b/build.zig @@ -0,0 +1,46 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const raylib = b.dependency("raylib", .{ + .target = target, + .optimize = optimize, + .shared = false, + }); + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + exe_mod.strip = false; + exe_mod.linkLibrary(raylib.artifact("raylib")); + + const exe = b.addExecutable(.{ + .name = "rex", + .root_module = exe_mod, + }); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_module = exe_mod, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..d6bf3cb --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .rex, + .version = "0.0.0", + .fingerprint = 0xee6003c042470dd0, + .minimum_zig_version = "0.14.0", + .dependencies = .{ + .raylib = .{ + .url = "git+https://github.com/raysan5/raylib#8d9c1cecb7f53aef720e2ee0d1558ffc39fa7eef", + .hash = "raylib-5.5.0-whq8uKXONARKPKfYVo14WktbUTIM3OYxdVPOPTdsQejc", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/build.zig.zon2json-lock b/build.zig.zon2json-lock new file mode 100644 index 0000000..559de1c --- /dev/null +++ b/build.zig.zon2json-lock @@ -0,0 +1,20 @@ +{ + "raylib-5.5.0-whq8uKXONARKPKfYVo14WktbUTIM3OYxdVPOPTdsQejc": { + "name": "raylib", + "url": "git+https://github.com/raysan5/raylib#8d9c1cecb7f53aef720e2ee0d1558ffc39fa7eef", + "hash": "sha256-NCUpCi5Kjs/mC97pAeRpPO4/1e5vaMZ2xEWrAXZV2Vs=", + "rev": "8d9c1cecb7f53aef720e2ee0d1558ffc39fa7eef" + }, + "N-V-__8AABHMqAWYuRdIlflwi8gksPnlUMQBiSxAqQAAZFms": { + "name": "xcode_frameworks", + "url": "git+https://github.com/hexops/xcode-frameworks#9a45f3ac977fd25dff77e58c6de1870b6808c4a7", + "hash": "sha256-xveFYoQu0BT+ZtEsyca/zdQ/so9jPK56SeX3xmF3iro=", + "rev": "9a45f3ac977fd25dff77e58c6de1870b6808c4a7" + }, + "N-V-__8AALRTBQDo_pUJ8IQ-XiIyYwDKQVwnr7-7o5kvPDGE": { + "name": "emsdk", + "url": "git+https://github.com/emscripten-core/emsdk#3.1.50", + "hash": "sha256-YUwb8yfz26Tfz4wyN13MBsdyA40ygJwfxHWt4eFMvQE=", + "rev": "e2627e265d940db5ea58dffa63e490375bfc92e5" + } +} \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c677b47 --- /dev/null +++ b/flake.lock @@ -0,0 +1,78 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1747273150, + "narHash": "sha256-8Vp3dEOmH4Idj0wnj66uJZbi4xiIgKxx+d6gD9KIBzc=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "7f8abfe980eb2ec5f3257c34aa4b4f0c3ca7002e", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "zig2nix": "zig2nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "zig2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1747273718, + "narHash": "sha256-cB0Abljln7nvmou3fecVKtIQZc0B8DpYBrw6CKbovgQ=", + "owner": "Cloudef", + "repo": "zig2nix", + "rev": "ea9a1a0f86a7addf8dedb03a38215b14c6f429b3", + "type": "github" + }, + "original": { + "owner": "Cloudef", + "repo": "zig2nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2da2297 --- /dev/null +++ b/flake.nix @@ -0,0 +1,90 @@ +{ + description = "Zig project flake"; + + inputs = { + zig2nix.url = "github:Cloudef/zig2nix"; + }; + + outputs = { zig2nix, ... }: let + flake-utils = zig2nix.inputs.flake-utils; + in (flake-utils.lib.eachDefaultSystem (system: let + # Zig flake helper + # Check the flake.nix in zig2nix project for more options: + # + env = zig2nix.outputs.zig-env.${system} {}; + in with builtins; with env.pkgs.lib; rec { + # Produces clean binaries meant to be ship'd outside of nix + # nix build .#foreign + packages.foreign = env.package { + src = cleanSource ./.; + + # Packages required for compiling + nativeBuildInputs = with env.pkgs; []; + + # Packages required for linking + buildInputs = with env.pkgs; [ + raylib + wayland + wayland-scanner + libxkbcommon + xorg.libXcursor + xorg.libXext + xorg.libXfixes + xorg.libXi + xorg.libXinerama + xorg.libXrandr + xorg.libXrender + ]; + + # Smaller binaries and avoids shipping glibc. + zigPreferMusl = true; + }; + + # nix build . + packages.default = packages.foreign.override (attrs: { + # Prefer nix friendly settings. + zigPreferMusl = false; + + # Executables required for runtime + # These packages will be added to the PATH + zigWrapperBins = with env.pkgs; []; + + # Libraries required for runtime + # These packages will be added to the LD_LIBRARY_PATH + zigWrapperLibs = attrs.buildInputs or []; + }); + + # For bundling with nix bundle for running outside of nix + # example: https://github.com/ralismark/nix-appimage + apps.bundle = { + type = "app"; + program = "${packages.foreign}/bin/default"; + }; + + # nix run . + apps.default = env.app [] "zig build run -- \"$@\""; + + # nix run .#build + apps.build = env.app [] "zig build \"$@\""; + + # nix run .#test + apps.test = env.app [] "zig build test -- \"$@\""; + + # nix run .#docs + apps.docs = env.app [] "zig build docs -- \"$@\""; + + # nix run .#zig2nix + apps.zig2nix = env.app [] "zig2nix \"$@\""; + + # nix develop + devShells.default = env.mkShell { + # Packages required for compiling, linking and running + # Libraries added here will be automatically added to the LD_LIBRARY_PATH and PKG_CONFIG_PATH + nativeBuildInputs = [] + ++ packages.default.nativeBuildInputs + ++ packages.default.buildInputs + ++ packages.default.zigWrapperBins + ++ packages.default.zigWrapperLibs; + }; + })); +} diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 0000000..38a9455 --- /dev/null +++ b/src/c.zig @@ -0,0 +1,4 @@ +pub const raylib = @cImport({ + @cInclude("raylib.h"); + @cInclude("raymath.h"); +}); diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..eb08aba --- /dev/null +++ b/src/main.zig @@ -0,0 +1,302 @@ +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + raylib.SetConfigFlags(raylib.FLAG_VSYNC_HINT | raylib.FLAG_WINDOW_RESIZABLE); + raylib.InitWindow(@intFromFloat(screen_width), @intFromFloat(screen_height), "ReX"); + defer raylib.CloseWindow(); + + raylib.SetWindowMinSize(480, 272); + raylib.SetWindowMaxSize(1920, 1080); + scales.recalculate(); + + global_font = raylib.LoadFontEx("font/SCE-PS3-RD-R-LATIN.TTF", 32, 0, 250); + raylib.SetTextureFilter(global_font.texture, raylib.TEXTURE_FILTER_TRILINEAR); + + // const camera = createCamera(); + + var background = Background.init(); + var column = Column.init( + allocator, + raylib.LoadTextureFromImage(raylib.GenImageChecked(64, 64, 8, 8, raylib.BLACK, raylib.WHITE)), + "Game", + ); + defer column.deinit(); + + var item = Item.init( + raylib.LoadTexture("menu/game/CometCrash/ICON0.PNG"), + "Comet Crash", + "", + ); + try column.appendItem(&item); + + raylib.SetTargetFPS(120); + while (!raylib.WindowShouldClose()) { + if (raylib.IsWindowResized()) { + screen_width = @floatFromInt(raylib.GetScreenWidth()); + screen_height = @floatFromInt(raylib.GetScreenHeight()); + scales.recalculate(); + } + if (raylib.IsKeyPressed('Z')) item.setBig(!item.big); + + raylib.BeginDrawing(); + defer raylib.EndDrawing(); + + background.draw(); + column.draw(); + + // { + // raylib.BeginMode3D(camera); + // defer raylib.EndMode3D(); + // } + + raylib.DrawFPS(1, 1); + + const debug_text = try std.fmt.allocPrint(allocator, "screen size = {d}x{d}", .{ screen_width, screen_height }); + defer allocator.free(debug_text); + raylib.DrawText(@ptrCast(debug_text), 80, 2, 8, raylib.GREEN); + } +} + +var global_font: raylib.Font = undefined; + +var screen_width: f32 = 480; +var screen_height: f32 = 272; + +var scales: Scales = undefined; + +pub const Scales = struct { + item_icon_scale: f32, + item_title_font_size: f32, + item_subtitle_font_size: f32, + + column_icon_scale: f32, + column_title_font_size: f32, + column_position_center: raylib.Vector2, + column_position_spacing: f32, + + pub fn recalculate(self: *Scales) void { + self.item_icon_scale = screen_height * 0.72 / screen_height; + + self.column_icon_scale = screen_height * 0.75 / screen_height; + self.column_title_font_size = screen_height * 13 / screen_height; + self.column_position_center = .{ + .x = std.math.lerp(0.0, screen_width, 0.18), + .y = std.math.lerp(0.0, screen_height, 0.15), + }; + self.column_position_spacing = 64; + } +}; + +pub const Column = struct { + icon: raylib.Texture2D, + title: []const u8, + + items: std.ArrayList(*Item), + + pub fn init(allocator: Allocator, icon: raylib.Texture2D, title: []const u8) Column { + raylib.SetTextureFilter(icon, raylib.TEXTURE_FILTER_BILINEAR); + return .{ + .icon = icon, + .title = title, + .items = .init(allocator), + }; + } + + pub fn deinit(self: *Column) void { + self.items.deinit(); + } + + pub fn draw(self: *Column) void { + const icon_position = scales.column_position_center; + const icon_scale = scales.column_icon_scale; + const icon_width = @as(f32, @floatFromInt(self.icon.width)) * icon_scale; + const icon_height = @as(f32, @floatFromInt(self.icon.height)) * icon_scale; + + const title_font_size = scales.column_title_font_size; + const title_font_spacing = 1.0; + const title_size = raylib.MeasureTextEx(global_font, @ptrCast(self.title), title_font_size, title_font_spacing); + const title_position = raylib.Vector2{ + .x = icon_position.x + icon_width / 2.0 - title_size.x / 2.0, + .y = icon_position.y + icon_height + 6, + }; + + var y: f32 = scales.column_position_center.y + icon_height + title_size.y + 32; + for (self.items.items) |item| { + item.position = .{ .x = scales.column_position_center.x, .y = y }; + // item.draw(); + y += 64; + } + + raylib.DrawTextureEx(self.icon, icon_position, 0, icon_scale, raylib.WHITE); + raylib.DrawTextEx(global_font, @ptrCast(self.title), title_position, title_font_size, title_font_spacing, raylib.WHITE); + } + + pub fn appendItem(self: *Column, item: *Item) !void { + try self.items.append(item); + } + + pub fn insertItem(self: *Column, idx: usize, item: *Item) !void { + try self.items.insert(idx, item); + } + + pub fn removeItem(self: *Column, idx: usize) void { + _ = try self.items.orderedRemove(idx); + } +}; + +pub const Item = struct { + time: f32 = 0.0, + start_scale: f32, + position: raylib.Vector2 = .{ .x = 0, .y = 0 }, + scale: f32, + + icon: raylib.Texture2D, + title: []const u8, + subtitle: []const u8, + big: bool = true, + + pub fn init(texture: raylib.Texture2D, title: []const u8, subtitle: []const u8) Item { + raylib.SetTextureFilter(texture, raylib.TEXTURE_FILTER_BILINEAR); + return .{ + .icon = texture, + .title = title, + .subtitle = subtitle, + .scale = scales.item_icon_scale, + .start_scale = scales.item_icon_scale, + }; + } + + pub fn draw(self: *Item) void { + self.time += raylib.GetFrameTime(); + self.scale = std.math.lerp( + self.start_scale, + if (self.big) scales.item_icon_scale else scales.item_icon_scale * 0.5, + easeOutExpo(self.time / 0.333), + ); + + const title_pos = raylib.Vector2{ + .x = self.position.x + 16 + @as(f32, @floatFromInt(self.icon.width)) * self.scale, + .y = (self.position.y + @as(f32, @floatFromInt(self.icon.height)) * self.scale) / 2.0, + }; + + raylib.DrawTextureEx(self.icon, self.position, 0, self.scale, raylib.WHITE); + raylib.DrawTextEx(global_font, @ptrCast(self.title), title_pos, 18.0, 1, raylib.WHITE); + } + + pub fn setBig(self: *Item, big: bool) void { + self.big = big; + self.time = 0; + self.start_scale = self.scale; + } + + fn easeOutExpo(x: f32) f32 { + return 1.0 - std.math.pow(f32, 2, -10 * std.math.clamp(x, 0.0, 1.0)); + } +}; + +/// Draws the dynamic gradient background. +// TODO shift based on time of day +// TODO image wallpaper +// TODO slideshow wallpaper +// TODO animated image wallpaper +pub const Background = struct { + top_left: Color, + top_right: Color, + bottom_right: Color, + bottom_left: Color, + + pub fn init() Background { + var self: Background = undefined; + self.setColors(NIGHT_08); + return self; + } + + pub fn setColors(self: *Background, colors: [4]Color) void { + self.top_left = colors[0]; + self.bottom_right = colors[1]; + self.top_right = colors[2]; + self.bottom_left = colors[3]; + } + + pub fn draw(self: *Background) void { + raylib.DrawRectangleGradientEx( + .{ + .x = 0, + .y = 0, + .width = screen_width, + .height = screen_height, + }, + self.top_left.toRaylib(), + self.bottom_left.toRaylib(), + self.top_right.toRaylib(), + self.bottom_right.toRaylib(), + ); + } + + pub const Color = struct { + r: f32, + g: f32, + b: f32, + + fn toRaylib(self: Color) raylib.Color { + return .{ + .r = @intFromFloat(self.r * 255.0), + .g = @intFromFloat(self.g * 255.0), + .b = @intFromFloat(self.b * 255.0), + .a = 255, + }; + } + }; + + pub const DAY_06 = [4]Color{ + .{ .r = 0.408, .g = 0.333, .b = 0.643 }, + .{ .r = 0.518, .g = 0.365, .b = 0.855 }, + .{ .r = 0.761, .g = 0.510, .b = 0.851 }, + .{ .r = 0.569, .g = 0.325, .b = 0.620 }, + }; + + pub const DAY_08 = [4]Color{ + .{ .r = 0.243, .g = 0.608, .b = 0.831 }, + .{ .r = 0.039, .g = 0.690, .b = 0.878 }, + .{ .r = 0.016, .g = 0.306, .b = 0.694 }, + .{ .r = 0.000, .g = 0.027, .b = 0.310 }, + }; + + pub const NIGHT_08 = [4]Color{ + .{ .r = 0.000, .g = 0.145, .b = 0.349 }, + .{ .r = 0.000, .g = 0.008, .b = 0.106 }, + .{ .r = 0.251, .g = 0.494, .b = 0.576 }, + .{ .r = 0.008, .g = 0.537, .b = 0.612 }, + }; +}; + +fn createCamera() raylib.Camera3D { + var camera = raylib.Camera3D{}; + + camera.position.x = 0; + camera.position.y = 10; + camera.position.z = 0; + + // camera pointing down, oriented correctly + // TODO this works but looks weird. do better. + camera.target.x = 0; + camera.target.y = 0; + camera.target.z = -0.000000000000001; + + camera.up.x = 0; + camera.up.y = 1; + camera.up.z = 0; + + camera.fovy = 1; + camera.projection = raylib.CAMERA_ORTHOGRAPHIC; + + return camera; +} + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const c = @import("c.zig"); +const raylib = c.raylib; diff --git a/src/rco.zig b/src/rco.zig new file mode 100644 index 0000000..7b5c66c --- /dev/null +++ b/src/rco.zig @@ -0,0 +1,352 @@ +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var rco_file = try std.fs.cwd().openFile("vsh/resource/explore_plugin_game.rco", .{}); + defer rco_file.close(); + + var rco = try RcoFile.init(allocator, rco_file); + defer rco.deinit(); + + try rco_file.seekTo(rco.toc_main_tree_offset.?); + const packed_size = try rco_file.reader().readInt(u32, .big); + const unpacked_size = try rco_file.reader().readInt(u32, .big); + const longest_text_size = try rco_file.reader().readInt(u32, .big); + std.debug.print("{d} {d} {d}\n", .{ packed_size, unpacked_size, longest_text_size }); + + const packed_buf = try allocator.alloc(u8, packed_size); + defer allocator.free(packed_buf); + _ = try rco_file.readAll(packed_buf); + + var fb = std.io.fixedBufferStream(packed_buf); + var al = std.ArrayList(u8).init(allocator); + defer al.deinit(); + + try std.compress.zlib.decompress(fb.reader(), al.writer()); + if (al.items.len != unpacked_size) return error.DecompressedSizeDoesNotMatch; + + var nfb = std.io.fixedBufferStream(al.items); + + while (nfb.pos < try nfb.getEndPos()) { + try parseTocEntry(nfb.reader()); + } + + std.debug.print("{any}\n", .{rco}); +} + +fn parseTocEntry(reader: anytype) !void { + const entry_type: TocEntryType = @enumFromInt(try reader.readInt(u16, .big)); + try reader.skipBytes(2, .{}); + const entry_label_offset = try reader.readInt(u32, .big); + const attributes_offset = try reader.readInt(u32, .big); + const children_offset = try reader.readInt(u32, .big); + const children_number = try reader.readInt(u32, .big); + const next_sibling_offset = try reader.readInt(u32, .big); + const prev_sibling_offset = try reader.readInt(u32, .big); + const parent_offset = try reader.readInt(u32, .big); + try reader.skipBytes(8, .{}); + + _ = .{ entry_label_offset, attributes_offset, children_offset, children_number, next_sibling_offset, prev_sibling_offset, parent_offset }; + + std.log.debug("Found TOC Entry: {any}", .{entry_type}); + + switch (entry_type) { + .MainTree, + .ScriptTree, + .TextTree, + .ImageTree, + .ModelTree, + .SoundTree, + .FontTree, + .ObjectTree, + .AnimTree, + => {}, + + .Text => { + const text = try RcoFile.Text.init(reader); + std.debug.print("Text: {any}\n", .{text}); + }, + + .Image => { + const image = try RcoFile.ImageTree.Image.init(reader); + std.debug.print("Image: {any}\n", .{image}); + }, + + else => {}, + } +} + +pub const TocEntryType = enum(u16) { + MainTree = 0x0101, + + ScriptTree = 0x0200, + + TextTree = 0x0300, + Text = 0x0301, + + ImageTree = 0x0400, + Image = 0x0401, + + ModelTree = 0x0500, + Model = 0x0501, + + SoundTree = 0x0600, + Sound = 0x0601, + + FontTree = 0x0700, + Font = 0x0701, + + ObjectTree = 0x0800, + Page = 0x0801, + Plane = 0x0802, + Button = 0x0803, + XMenu, + XMList, + Progress, + Scroll, + MList, + MItem, + _object_unk1 = 0x080B, + XItem, + TextObject, + ModelObject, + Spin, + Action, + ItemSpin, + Group, + LList, + LItem, + Edit, + Clock, + IList, + IItem, + Icon, + UButton, + _object_unk2 = 0x081B, + CheckboxGroup, + CheckboxItem, + Meter, + EditBox = 0x081F, + + AnimTree = 0x0900, + Anim = 0x0901, + MoveTo = 0x0902, + Recolour = 0x0903, + Rotate = 0x0904, + Resize = 0x0905, + Fade = 0x0906, + Delay = 0x0907, + FireEvent = 0x0908, + Lock = 0x0909, + Unlock = 0x090A, + SlideOut = 0x090B, + + _, +}; + +pub const RcoFile = struct { + allocator: Allocator, + file: std.fs.File, + + compression: Compression, + + toc_main_tree_offset: ?u32, + toc_script_tree_offset: ?u32, + toc_text_tree_offset: ?u32, + toc_sound_tree_offset: ?u32, + toc_model_tree_offset: ?u32, + toc_image_tree_offset: ?u32, + toc_font_tree_offset: ?u32, + toc_object_tree_offset: ?u32, + toc_anim_tree_offset: ?u32, + + pub fn parse(self: *RcoFile) !void { + const rco_header = try self.file.reader().readStructEndian(RcoHeader, .big); + self.compression = @enumFromInt(rco_header.prf_compress); + self.toc_main_tree_offset = parseOffset(rco_header.toc_main_tree_offset); + self.toc_script_tree_offset = parseOffset(rco_header.toc_script_tree_offset); + self.toc_text_tree_offset = parseOffset(rco_header.toc_text_tree_offset); + self.toc_sound_tree_offset = parseOffset(rco_header.toc_sound_tree_offset); + self.toc_model_tree_offset = parseOffset(rco_header.toc_model_tree_offset); + self.toc_image_tree_offset = parseOffset(rco_header.toc_image_tree_offset); + self.toc_font_tree_offset = parseOffset(rco_header.toc_font_tree_offset); + self.toc_object_tree_offset = parseOffset(rco_header.toc_object_tree_offset); + self.toc_anim_tree_offset = parseOffset(rco_header.toc_anim_tree_offset); + } + + pub fn init(allocator: Allocator, file: std.fs.File) !RcoFile { + var self: RcoFile = undefined; + self.allocator = allocator; + self.file = file; + try self.parse(); + return self; + } + + pub fn deinit(_: *RcoFile) void {} + + fn parseOffset(offset: u32) ?u32 { + return if (offset == std.math.maxInt(u32)) null else offset; + } + + pub fn format( + self: RcoFile, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.writeAll("RcoFile{\n"); + try writer.print(" .compression = {any},\n", .{self.compression}); + try self.fmtField("toc_main_tree_offset", writer); + try self.fmtField("toc_script_tree_offset", writer); + try self.fmtField("toc_text_tree_offset", writer); + try self.fmtField("toc_sound_tree_offset", writer); + try self.fmtField("toc_model_tree_offset", writer); + try self.fmtField("toc_image_tree_offset", writer); + try self.fmtField("toc_font_tree_offset", writer); + try self.fmtField("toc_object_tree_offset", writer); + try self.fmtField("toc_anim_tree_offset", writer); + try writer.writeAll("}"); + } + + inline fn fmtField(self: RcoFile, comptime field_name: []const u8, writer: anytype) !void { + const field = @field(self, field_name); + const format_string = std.fmt.comptimePrint(" .{s} = {{s}}{{?X}},\n", .{field_name}); + try writer.print(format_string, .{ + if (field != null) "0x" else "", + field, + }); + } + + pub const Compression = enum(u32) { + none = 0, + zlib = 0x10, + rlz = 0x20, + }; + + pub const Text = struct { + language: Language, + encoding: Encoding, + number_of: u32, + + pub const Language = enum(u16) { + Japanese = 0, + English_US = 1, + French = 2, + Spanish_Spain = 3, + German = 4, + Italian = 5, + Dutch = 6, + Portuguese_Portugal = 7, + Russian = 8, + Korean = 9, + Chinese_Traditional = 10, + Chinese_Simplified = 11, + Finnish = 12, + Swedish = 13, + Danish = 14, + Norwegian = 15, + Polish = 16, + Portuguese_Brazil = 17, + English_UK = 18, + Turkish = 19, + }; + + pub const Encoding = enum(u16) { + utf8 = 0, + utf16 = 1, + utf32 = 2, + }; + + pub const String = extern struct { + label_offset: u32, + string_length: u32, + string_offset: u32, + }; + }; + + pub const ImageTree = struct { + pub const Image = struct { + format: Format, + compression: Image.Compression, + size: u32, + offset: u32, + uncompressed_size: ?u32, + + pub fn init(reader: anytype) !Image { + var self: Image = undefined; + self.format = @enumFromInt(try reader.readInt(u16, .big)); + self.compression = @enumFromInt(try reader.readInt(u16, .big)); + self.size = try reader.readInt(u32, .big); + self.offset = try reader.readInt(u32, .big); + try reader.skipBytes(4, .{}); + self.uncompressed_size = if (self.compression == .none) null else try reader.readInt(u32, .big); + return self; + } + + pub const Format = enum(u16) { + png = 0, + jpeg = 1, + tiff = 2, + gif = 3, + bmp = 4, + gim = 5, + }; + + pub const Compression = enum(u16) { + none = 0, + zlib = 1, + rlz = 2, + }; + }; + }; +}; + +pub const RcoHeader = extern struct { + prf_signature: [4]u8, + prf_version: [4]u8, + prf_unk: u32 = 0, + prf_compress: u32, + + toc_main_tree_offset: u32, + toc_script_tree_offset: u32, + toc_text_tree_offset: u32, + toc_sound_tree_offset: u32, + toc_model_tree_offset: u32, + toc_image_tree_offset: u32, + toc_unk: u32 = std.math.maxInt(u32), + toc_font_tree_offset: u32, + toc_object_tree_offset: u32, + toc_anim_tree_offset: u32, + + str_text_table_offset: u32, + str_text_table_length: u32, + str_text_label_offset: u32, + str_text_label_length: u32, + str_text_event_offset: u32, + str_text_event_length: u32, + + ptr_text_table_offset: u32, + ptr_text_table_length: u32, + ptr_image_table_offset: u32, + ptr_image_table_length: u32, + ptr_model_table_offset: u32, + ptr_model_table_length: u32, + ptr_sound_table_offset: u32, + ptr_sound_table_length: u32, + ptr_object_table_offset: u32, + ptr_object_table_length: u32, + ptr_anim_table_offset: u32, + ptr_anim_table_length: u32, + + dat_image_table_offset: u32, + dat_image_table_length: u32, + dat_sound_table_offset: u32, + dat_sound_table_length: u32, + dat_model_table_offset: u32, + dat_model_table_length: u32, + dat_unk: [3]u32 = [_]u32{std.math.maxInt(u32)} ** 3, +}; + +const std = @import("std"); +const Allocator = std.mem.Allocator;