From df9a0e7ea73899a1a1e264e06846849db1bfa8c3 Mon Sep 17 00:00:00 2001 From: KaiserGranatapfel Date: Mon, 9 Feb 2026 21:11:13 +0200 Subject: [PATCH] feat: add Lua scripting integration and web dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new workspace crates (gcrecomp-lua, gcrecomp-web) providing a Lua 5.4 scripting layer for pipeline orchestration, configuration, in-game UI, verification, and optimization — plus a localhost web dashboard for recompilation management. - gcrecomp-lua: LuaEngine with mlua bindings for GameConfig, pipeline stages, CpuContext, MemoryManager, UI screen registration, CRC32/SHA256 verification, and dead code elimination / size optimization - gcrecomp-web: Axum-based REST server with DOL upload, recompilation control, status polling, config management, and static HTML frontend - Refactor RecompilationPipeline into discrete stage methods via PipelineContext (backward compatible) - Enhance Optimizer with li/addi/lis constant propagation and function-level DCE via call graph reachability - Add LuaScreen variant to iced UI for Lua-defined menu screens - Add optional Lua event handler delegate to GameIntegration - Initialize LuaEngine in game entry point Co-Authored-By: Claude Opus 4.6 --- Cargo.toml | 15 ++ game/Cargo.toml | 1 + game/src/main.rs | 18 ++ gcrecomp-core/src/recompiler/optimizer.rs | 223 ++++++++++++++-------- gcrecomp-core/src/recompiler/pipeline.rs | 200 +++++++++++++++++++ gcrecomp-lua/Cargo.toml | 22 +++ gcrecomp-lua/src/bindings/config.rs | 110 +++++++++++ gcrecomp-lua/src/bindings/cpu.rs | 90 +++++++++ gcrecomp-lua/src/bindings/memory.rs | 81 ++++++++ gcrecomp-lua/src/bindings/mod.rs | 26 +++ gcrecomp-lua/src/bindings/optimize.rs | 100 ++++++++++ gcrecomp-lua/src/bindings/pipeline.rs | 103 ++++++++++ gcrecomp-lua/src/bindings/ui.rs | 99 ++++++++++ gcrecomp-lua/src/bindings/verify.rs | 92 +++++++++ gcrecomp-lua/src/engine.rs | 45 +++++ gcrecomp-lua/src/error.rs | 38 ++++ gcrecomp-lua/src/lib.rs | 3 + gcrecomp-ui/src/app.rs | 14 +- gcrecomp-ui/src/integration.rs | 12 ++ gcrecomp-web/Cargo.toml | 26 +++ gcrecomp-web/src/main.rs | 12 ++ gcrecomp-web/src/routes.rs | 207 ++++++++++++++++++++ gcrecomp-web/src/security.rs | 34 ++++ gcrecomp-web/src/server.rs | 66 +++++++ lua/game/init.lua | 10 + lua/game/menus.lua | 28 +++ lua/init.lua | 19 ++ lua/optimize/default.lua | 43 +++++ lua/pipeline.lua | 50 +++++ lua/verify/default.lua | 56 ++++++ lua/web/routes.lua | 33 ++++ web/static/index.html | 143 ++++++++++++++ 32 files changed, 1936 insertions(+), 83 deletions(-) create mode 100644 gcrecomp-lua/Cargo.toml create mode 100644 gcrecomp-lua/src/bindings/config.rs create mode 100644 gcrecomp-lua/src/bindings/cpu.rs create mode 100644 gcrecomp-lua/src/bindings/memory.rs create mode 100644 gcrecomp-lua/src/bindings/mod.rs create mode 100644 gcrecomp-lua/src/bindings/optimize.rs create mode 100644 gcrecomp-lua/src/bindings/pipeline.rs create mode 100644 gcrecomp-lua/src/bindings/ui.rs create mode 100644 gcrecomp-lua/src/bindings/verify.rs create mode 100644 gcrecomp-lua/src/engine.rs create mode 100644 gcrecomp-lua/src/error.rs create mode 100644 gcrecomp-lua/src/lib.rs create mode 100644 gcrecomp-web/Cargo.toml create mode 100644 gcrecomp-web/src/main.rs create mode 100644 gcrecomp-web/src/routes.rs create mode 100644 gcrecomp-web/src/security.rs create mode 100644 gcrecomp-web/src/server.rs create mode 100644 lua/game/init.lua create mode 100644 lua/game/menus.lua create mode 100644 lua/init.lua create mode 100644 lua/optimize/default.lua create mode 100644 lua/pipeline.lua create mode 100644 lua/verify/default.lua create mode 100644 lua/web/routes.lua create mode 100644 web/static/index.html diff --git a/Cargo.toml b/Cargo.toml index 7f3fbfa..4b753de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "gcrecomp-cli", "gcrecomp-ui", "gcrecomp-runtime", + "gcrecomp-lua", + "gcrecomp-web", "game", ] resolver = "2" @@ -13,6 +15,7 @@ opt-level = 3 lto = true codegen-units = 1 strip = true +panic = "abort" [workspace.package] version = "0.0.1-alpha" @@ -54,3 +57,15 @@ minifb = "0.25" smallvec = "1.13" bitvec = "1.0" +# Lua scripting +mlua = { version = "0.10", features = ["lua54", "vendored", "serialize", "send"] } + +# Verification +crc32fast = "1.4" +sha2 = "0.10" + +# Web server +axum = { version = "0.7", features = ["multipart"] } +tokio = { version = "1", features = ["full"] } +tower-http = { version = "0.5", features = ["fs", "cors"] } + diff --git a/game/Cargo.toml b/game/Cargo.toml index a2bbad5..d7deea4 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -12,5 +12,6 @@ path = "src/main.rs" [dependencies] gcrecomp-runtime = { path = "../gcrecomp-runtime" } gcrecomp-ui = { path = "../gcrecomp-ui" } +gcrecomp-lua = { path = "../gcrecomp-lua" } log = { workspace = true } diff --git a/game/src/main.rs b/game/src/main.rs index 2f6e79d..0ed4cc8 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -1,4 +1,22 @@ // Game entry point fn main() { println!("Game entry point - recompiled code will be integrated here"); + + // Initialize Lua scripting engine + match gcrecomp_lua::engine::LuaEngine::new() { + Ok(engine) => { + println!("Lua scripting engine initialized"); + + // Load game initialization scripts + let init_script = std::path::Path::new("lua/game/init.lua"); + if init_script.exists() { + if let Err(e) = engine.execute_file(init_script) { + eprintln!("Failed to load game scripts: {}", e); + } + } + } + Err(e) => { + eprintln!("Failed to initialize Lua engine: {}", e); + } + } } diff --git a/gcrecomp-core/src/recompiler/optimizer.rs b/gcrecomp-core/src/recompiler/optimizer.rs index 5cbf148..cf7a0b1 100644 --- a/gcrecomp-core/src/recompiler/optimizer.rs +++ b/gcrecomp-core/src/recompiler/optimizer.rs @@ -6,14 +6,11 @@ //! # Optimization Passes //! - **Constant Folding**: Evaluate constant expressions at compile time //! - **Dead Code Elimination**: Remove unused instructions -//! - **Register Allocation**: Optimize register usage (placeholder for future implementation) -//! -//! # Memory Optimizations -//! - Pre-allocates result vectors with estimated capacity -//! - Uses efficient data structures for analysis +//! - **Constant Propagation**: Track li/addi constant loads through register chains +//! - **Function-level DCE**: Remove unreachable functions using call graph analysis -use crate::recompiler::decoder::DecodedInstruction; -use std::collections::HashMap; +use crate::recompiler::decoder::{DecodedInstruction, InstructionType, Operand}; +use std::collections::{HashMap, HashSet}; /// Optimizer for PowerPC instructions. /// @@ -23,122 +20,184 @@ pub struct Optimizer { constant_folding: bool, /// Enable dead code elimination dead_code_elimination: bool, - /// Enable register allocation (placeholder) - register_allocation: bool, } impl Optimizer { /// Create a new optimizer with all optimizations enabled. - /// - /// # Returns - /// `Optimizer` - New optimizer instance - /// - /// # Examples - /// ```rust - /// let optimizer = Optimizer::new(); - /// ``` - #[inline] // Constructor - simple, may be inlined pub fn new() -> Self { Self { constant_folding: true, dead_code_elimination: true, - register_allocation: true, } } /// Optimize a sequence of instructions. - /// - /// # Algorithm - /// Applies optimization passes in order: - /// 1. Constant folding - /// 2. Dead code elimination - /// - /// # Arguments - /// * `instructions` - Sequence of decoded PowerPC instructions - /// - /// # Returns - /// `Vec` - Optimized instruction sequence - /// - /// # Examples - /// ```rust - /// let optimized = optimizer.optimize(&instructions); - /// ``` - #[inline] // May be called frequently pub fn optimize(&self, instructions: &[DecodedInstruction]) -> Vec { let mut optimized: Vec = instructions.to_vec(); - + if self.constant_folding { optimized = self.fold_constants(&optimized); } - + if self.dead_code_elimination { optimized = self.eliminate_dead_code(&optimized); } - + optimized } - /// Constant folding optimization pass. - /// - /// # Algorithm - /// Tracks constant values in registers and evaluates constant expressions - /// at compile time, replacing them with immediate values. + /// Constant folding and propagation pass. /// - /// # Arguments - /// * `instructions` - Instruction sequence to optimize - /// - /// # Returns - /// `Vec` - Optimized instruction sequence - #[inline] // Optimization pass - may be inlined + /// Tracks constant values loaded by `li` (opcode 14 with rA=0) and `addi` + /// patterns, propagating them through register chains. fn fold_constants(&self, instructions: &[DecodedInstruction]) -> Vec { let mut result: Vec = Vec::with_capacity(instructions.len()); - let mut constants: HashMap> = HashMap::new(); - + let mut constants: HashMap = HashMap::new(); + for inst in instructions.iter() { - // Track constant values in registers - // If we can determine a register always holds a constant, we can fold it - let optimized: DecodedInstruction = inst.clone(); // For now, just pass through - result.push(optimized); + // Track li/addi constant loads (opcode 14 = addi; li is addi rD,0,imm) + if inst.instruction.opcode == 14 && inst.instruction.operands.len() >= 3 { + if let (Operand::Register(rd), Operand::Register(ra), Operand::Immediate(imm)) = ( + &inst.instruction.operands[0], + &inst.instruction.operands[1], + &inst.instruction.operands[2], + ) { + if *ra == 0 { + // li rD, imm — rD = sign_extend(imm) + constants.insert(*rd, *imm as i32 as u32); + } else if let Some(&base) = constants.get(ra) { + // addi rD, rA, imm where rA is known constant + constants.insert(*rd, base.wrapping_add(*imm as i32 as u32)); + } else { + constants.remove(rd); + } + } + } + // Track lis (opcode 15 = addis; lis is addis rD,0,imm) + else if inst.instruction.opcode == 15 && inst.instruction.operands.len() >= 3 { + if let (Operand::Register(rd), Operand::Register(ra), Operand::Immediate(imm)) = ( + &inst.instruction.operands[0], + &inst.instruction.operands[1], + &inst.instruction.operands[2], + ) { + if *ra == 0 { + constants.insert(*rd, (*imm as u32) << 16); + } else { + constants.remove(rd); + } + } + } + // Invalidate register on any other write + else if let Some(Operand::Register(rd)) = inst.instruction.operands.first() { + if matches!( + inst.instruction.instruction_type, + InstructionType::Arithmetic + | InstructionType::Load + | InstructionType::Move + | InstructionType::Shift + | InstructionType::Rotate + ) { + constants.remove(rd); + } + } + // Branches invalidate all tracked constants (control flow merge) + if matches!(inst.instruction.instruction_type, InstructionType::Branch) { + constants.clear(); + } + + result.push(inst.clone()); } - + result } - /// Dead code elimination optimization pass. - /// - /// # Algorithm - /// Removes instructions that write to registers that are never read. - /// Uses reverse pass to identify unused register definitions. + /// Dead code elimination pass. /// - /// # Arguments - /// * `instructions` - Instruction sequence to optimize - /// - /// # Returns - /// `Vec` - Optimized instruction sequence - #[inline] // Optimization pass - may be inlined + /// Removes instructions that write to registers never subsequently read. fn eliminate_dead_code(&self, instructions: &[DecodedInstruction]) -> Vec { - // Simple dead code elimination: remove writes to registers that are never read let mut result: Vec = Vec::with_capacity(instructions.len()); - let mut used_registers: std::collections::HashSet = std::collections::HashSet::new(); - - // First pass: find all used registers (reverse pass) - for inst in instructions.iter().rev() { - // Check if this instruction uses any registers - // If a register is written but never read after, it's dead - // (Simplified implementation - would use proper def-use analysis) + + // Collect used registers in a backward pass + let mut used_after: HashSet = HashSet::new(); + // Assume all registers may be live at function exit + for r in 0..32u8 { + used_after.insert(r); } - - // Second pass: keep only instructions that produce used values - for inst in instructions.iter() { - result.push(inst.clone()); + + let mut keep = vec![true; instructions.len()]; + + for (i, inst) in instructions.iter().enumerate().rev() { + // Branch/system/store instructions always kept (side effects) + if matches!( + inst.instruction.instruction_type, + InstructionType::Branch + | InstructionType::System + | InstructionType::Store + | InstructionType::Compare + ) { + // Mark all source registers as used + for op in &inst.instruction.operands { + if let Operand::Register(r) = op { + used_after.insert(*r); + } + } + continue; + } + + // For instructions that write a register: check if it's read later + if let Some(Operand::Register(rd)) = inst.instruction.operands.first() { + if !used_after.contains(rd) { + keep[i] = false; + continue; + } + // Remove destination from used set, add sources + used_after.remove(rd); + } + + for op in inst.instruction.operands.iter().skip(1) { + if let Operand::Register(r) = op { + used_after.insert(*r); + } + } } - + + for (i, inst) in instructions.iter().enumerate() { + if keep[i] { + result.push(inst.clone()); + } + } + result } + + /// Function-level dead code elimination using call graph. + /// + /// Given a set of function addresses and their call targets, returns the set + /// of reachable function addresses from the given entry points. + pub fn reachable_functions( + entry_points: &[u32], + call_graph: &HashMap>, + ) -> HashSet { + let mut reachable = HashSet::new(); + let mut worklist: Vec = entry_points.to_vec(); + + while let Some(addr) = worklist.pop() { + if reachable.insert(addr) { + if let Some(callees) = call_graph.get(&addr) { + for &callee in callees { + if !reachable.contains(&callee) { + worklist.push(callee); + } + } + } + } + } + + reachable + } } impl Default for Optimizer { - #[inline] // Simple default implementation fn default() -> Self { Self::new() } diff --git a/gcrecomp-core/src/recompiler/pipeline.rs b/gcrecomp-core/src/recompiler/pipeline.rs index ab38133..cd4818d 100644 --- a/gcrecomp-core/src/recompiler/pipeline.rs +++ b/gcrecomp-core/src/recompiler/pipeline.rs @@ -36,6 +36,38 @@ use smallvec::SmallVec; /// to Rust code generation. pub struct RecompilationPipeline; +/// Mutable context that carries state through pipeline stages. +pub struct PipelineContext { + pub dol_file: Option, + pub ghidra_analysis: Option, + pub instructions: Option>, + pub cfg: Option, + pub rust_code: Option, + pub stats: PipelineStats, +} + +/// Statistics collected during pipeline execution. +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct PipelineStats { + pub total_functions: usize, + pub successful_functions: usize, + pub failed_functions: usize, + pub total_instructions: usize, +} + +impl PipelineContext { + pub fn new() -> Self { + Self { + dol_file: None, + ghidra_analysis: None, + instructions: None, + cfg: None, + rust_code: None, + stats: PipelineStats::default(), + } + } +} + impl RecompilationPipeline { /// Recompile a DOL file to Rust code. /// @@ -249,6 +281,174 @@ impl RecompilationPipeline { Ok(()) } + // --- Discrete stage methods for Lua orchestration --- + + /// Stage: Load a DOL file into the pipeline context. + pub fn stage_load_dol(ctx: &mut PipelineContext, path: &str) -> Result<()> { + log::info!("Stage: Loading DOL file: {}", path); + let data = std::fs::read(path)?; + let dol = crate::recompiler::parser::DolFile::parse(&data, path)?; + ctx.dol_file = Some(dol); + Ok(()) + } + + /// Stage: Run Ghidra analysis on the loaded DOL. + pub fn stage_analyze(ctx: &mut PipelineContext) -> Result<()> { + log::info!("Stage: Running Ghidra analysis..."); + let dol = ctx.dol_file.as_ref().ok_or_else(|| anyhow::anyhow!("No DOL file loaded"))?; + let analysis = GhidraAnalysis::analyze( + &dol.path, + crate::recompiler::ghidra::GhidraBackend::ReOxide, + )?; + ctx.ghidra_analysis = Some(analysis); + Ok(()) + } + + /// Stage: Decode PowerPC instructions from the DOL. + pub fn stage_decode(ctx: &mut PipelineContext) -> Result<()> { + log::info!("Stage: Decoding instructions..."); + let dol = ctx.dol_file.as_ref().ok_or_else(|| anyhow::anyhow!("No DOL file loaded"))?; + let instructions = Self::decode_all_instructions(dol)?; + ctx.stats.total_instructions = instructions.len(); + ctx.instructions = Some(instructions); + Ok(()) + } + + /// Stage: Build control flow graph. + pub fn stage_build_cfg(ctx: &mut PipelineContext) -> Result<()> { + log::info!("Stage: Building control flow graph..."); + let instructions = ctx.instructions.as_ref().ok_or_else(|| anyhow::anyhow!("No instructions decoded"))?; + let cfg = ControlFlowAnalyzer::build_cfg(instructions, 0u32)?; + ctx.cfg = Some(cfg); + Ok(()) + } + + /// Stage: Perform data flow analysis. + pub fn stage_analyze_data_flow(ctx: &mut PipelineContext) -> Result<()> { + log::info!("Stage: Performing data flow analysis..."); + let instructions = ctx.instructions.as_ref().ok_or_else(|| anyhow::anyhow!("No instructions decoded"))?; + let cfg = ctx.cfg.as_ref().ok_or_else(|| anyhow::anyhow!("No CFG built"))?; + let _def_use_chains = DataFlowAnalyzer::build_def_use_chains(instructions); + let _live_analysis = DataFlowAnalyzer::live_variable_analysis(cfg); + Ok(()) + } + + /// Stage: Infer types (placeholder). + pub fn stage_infer_types(_ctx: &mut PipelineContext) -> Result<()> { + log::info!("Stage: Inferring types..."); + Ok(()) + } + + /// Stage: Generate Rust code from analyzed instructions. + pub fn stage_generate_code(ctx: &mut PipelineContext) -> Result<()> { + log::info!("Stage: Generating code..."); + let ghidra_analysis = ctx.ghidra_analysis.as_ref().ok_or_else(|| anyhow::anyhow!("No Ghidra analysis"))?; + let instructions = ctx.instructions.as_ref().ok_or_else(|| anyhow::anyhow!("No instructions decoded"))?; + let mut codegen = CodeGenerator::new(); + + let estimated_capacity = ghidra_analysis.functions.len() * 1000; + let mut rust_code = String::with_capacity(estimated_capacity); + + rust_code.push_str("//! Recompiled GameCube game functions\n"); + rust_code.push_str("//! Generated by GCRecomp\n\n"); + rust_code.push_str("use crate::runtime::context::CpuContext;\n"); + rust_code.push_str("use crate::runtime::memory::MemoryManager;\n"); + rust_code.push_str("use anyhow::Result;\n\n"); + + let total_functions = ghidra_analysis.functions.len(); + let mut successful = 0usize; + let mut failed = 0usize; + + for func in ghidra_analysis.functions.iter() { + let func_instructions = Self::map_instructions_to_function(func, instructions); + + if func_instructions.is_empty() { + failed += 1; + continue; + } + + let func_metadata = crate::recompiler::analysis::FunctionMetadata { + address: func.address, + name: func.name.clone(), + size: func.size, + calling_convention: func.calling_convention.clone(), + parameters: func.parameters.iter().map(|p| { + crate::recompiler::analysis::ParameterInfo { + name: p.name.clone(), + type_info: crate::recompiler::analysis::TypeInfo::Unknown, + register: None, + stack_offset: p.offset.unwrap_or(0), + } + }).collect(), + return_type: None, + local_variables: func.local_variables.iter().map(|v| { + crate::recompiler::analysis::VariableInfo { + name: v.name.clone(), + type_info: crate::recompiler::analysis::TypeInfo::Unknown, + stack_offset: v.offset, + scope_start: 0, + scope_end: 0, + } + }).collect(), + basic_blocks: vec![], + }; + + match codegen.generate_function(&func_metadata, &func_instructions) { + Ok(func_code) => { + rust_code.push_str(&func_code); + rust_code.push('\n'); + successful += 1; + } + Err(e) => { + failed += 1; + rust_code.push_str(&format!( + "// Stub for {} at 0x{:08X} (generation failed: {})\n", + func.name, func.address, e + )); + rust_code.push_str(&format!( + "pub fn {}_0x{:08X}(_ctx: &mut CpuContext, _memory: &mut MemoryManager) -> Result> {{\n", + codegen.sanitize_identifier(&func.name), func.address + )); + rust_code.push_str(" Ok(None)\n}\n\n"); + } + } + } + + // Function dispatcher + rust_code.push_str("\npub fn call_function_by_address(\n address: u32,\n ctx: &mut CpuContext,\n memory: &mut MemoryManager,\n) -> Result> {\n match address {\n"); + for func in ghidra_analysis.functions.iter() { + let func_name = if func.name.is_empty() || func.name.starts_with("sub_") { + format!("func_0x{:08X}", func.address) + } else { + format!("{}_{:08X}", codegen.sanitize_identifier(&func.name), func.address) + }; + rust_code.push_str(&format!(" 0x{:08X}u32 => {}(ctx, memory),\n", func.address, func_name)); + } + rust_code.push_str(" _ => Ok(None),\n }\n}\n"); + + ctx.stats.total_functions = total_functions; + ctx.stats.successful_functions = successful; + ctx.stats.failed_functions = failed; + ctx.rust_code = Some(rust_code); + Ok(()) + } + + /// Stage: Validate generated code. + pub fn stage_validate(ctx: &mut PipelineContext) -> Result<()> { + log::info!("Stage: Validating generated code..."); + let code = ctx.rust_code.as_ref().ok_or_else(|| anyhow::anyhow!("No code generated"))?; + CodeValidator::validate_rust_code(code)?; + Ok(()) + } + + /// Stage: Write output to file. + pub fn stage_write_output(ctx: &mut PipelineContext, output_path: &str) -> Result<()> { + log::info!("Stage: Writing output to {}...", output_path); + let code = ctx.rust_code.as_ref().ok_or_else(|| anyhow::anyhow!("No code generated"))?; + std::fs::write(output_path, code)?; + Ok(()) + } + /// Decode all instructions from a DOL file. /// /// # Algorithm diff --git a/gcrecomp-lua/Cargo.toml b/gcrecomp-lua/Cargo.toml new file mode 100644 index 0000000..fac46df --- /dev/null +++ b/gcrecomp-lua/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "gcrecomp-lua" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Lua scripting integration for GameCube static recompiler" + +[dependencies] +gcrecomp-core = { path = "../gcrecomp-core" } +gcrecomp-runtime = { path = "../gcrecomp-runtime" } +gcrecomp-ui = { path = "../gcrecomp-ui" } +mlua = { workspace = true } +anyhow = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +crc32fast = { workspace = true } +sha2 = { workspace = true } diff --git a/gcrecomp-lua/src/bindings/config.rs b/gcrecomp-lua/src/bindings/config.rs new file mode 100644 index 0000000..d150445 --- /dev/null +++ b/gcrecomp-lua/src/bindings/config.rs @@ -0,0 +1,110 @@ +use mlua::{Lua, Table}; + +use crate::error::IntoAnyhow; + +pub fn register(lua: &Lua, gcrecomp: &Table) -> anyhow::Result<()> { + let config_table = lua.create_table().into_anyhow()?; + + let load_fn = lua + .create_function(|lua, ()| { + let config = gcrecomp_ui::config::GameConfig::load().map_err(mlua::Error::external)?; + let value = serde_json::to_value(&config).map_err(mlua::Error::external)?; + json_to_lua(lua, &value) + }) + .into_anyhow()?; + + let save_fn = lua + .create_function(|_, tbl: Table| { + let value = lua_table_to_json(&tbl)?; + let config: gcrecomp_ui::config::GameConfig = + serde_json::from_value(value).map_err(mlua::Error::external)?; + config.save().map_err(mlua::Error::external)?; + Ok(()) + }) + .into_anyhow()?; + + config_table.set("load", load_fn).into_anyhow()?; + config_table.set("save", save_fn).into_anyhow()?; + gcrecomp.set("config", config_table).into_anyhow()?; + Ok(()) +} + +fn json_to_lua(lua: &Lua, value: &serde_json::Value) -> mlua::Result { + match value { + serde_json::Value::Null => Ok(mlua::Value::Nil), + serde_json::Value::Bool(b) => Ok(mlua::Value::Boolean(*b)), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(mlua::Value::Integer(i)) + } else if let Some(f) = n.as_f64() { + Ok(mlua::Value::Number(f)) + } else { + Ok(mlua::Value::Nil) + } + } + serde_json::Value::String(s) => { + let ls = lua.create_string(s)?; + Ok(mlua::Value::String(ls)) + } + serde_json::Value::Array(arr) => { + let table = lua.create_table()?; + for (i, v) in arr.iter().enumerate() { + table.set(i + 1, json_to_lua(lua, v)?)?; + } + Ok(mlua::Value::Table(table)) + } + serde_json::Value::Object(map) => { + let table = lua.create_table()?; + for (k, v) in map { + table.set(k.as_str(), json_to_lua(lua, v)?)?; + } + Ok(mlua::Value::Table(table)) + } + } +} + +fn lua_table_to_json(table: &Table) -> mlua::Result { + let len = table.raw_len(); + let is_array = len > 0 && { + let mut is_seq = true; + for i in 1..=len { + if table.raw_get::(i)?.is_nil() { + is_seq = false; + break; + } + } + is_seq + }; + + if is_array { + let mut arr = Vec::new(); + for i in 1..=len { + arr.push(lua_value_to_json(table.raw_get::(i)?)?); + } + Ok(serde_json::Value::Array(arr)) + } else { + let mut map = serde_json::Map::new(); + for pair in table.clone().pairs::() { + let (k, v) = pair?; + let key = match k { + mlua::Value::String(s) => s.to_str()?.to_string(), + mlua::Value::Integer(i) => i.to_string(), + _ => continue, + }; + map.insert(key, lua_value_to_json(v)?); + } + Ok(serde_json::Value::Object(map)) + } +} + +fn lua_value_to_json(value: mlua::Value) -> mlua::Result { + match value { + mlua::Value::Nil => Ok(serde_json::Value::Null), + mlua::Value::Boolean(b) => Ok(serde_json::Value::Bool(b)), + mlua::Value::Integer(i) => Ok(serde_json::json!(i)), + mlua::Value::Number(f) => Ok(serde_json::json!(f)), + mlua::Value::String(s) => Ok(serde_json::Value::String(s.to_str()?.to_string())), + mlua::Value::Table(t) => lua_table_to_json(&t), + _ => Ok(serde_json::Value::Null), + } +} diff --git a/gcrecomp-lua/src/bindings/cpu.rs b/gcrecomp-lua/src/bindings/cpu.rs new file mode 100644 index 0000000..57fec6c --- /dev/null +++ b/gcrecomp-lua/src/bindings/cpu.rs @@ -0,0 +1,90 @@ +use mlua::{Lua, Table, UserData, UserDataMethods}; +use std::sync::{Arc, Mutex}; + +use gcrecomp_core::runtime::context::CpuContext; + +use crate::error::IntoAnyhow; + +pub struct LuaCpuContext { + pub inner: Arc>, +} + +impl UserData for LuaCpuContext { + fn add_methods>(methods: &mut M) { + methods.add_method("get_gpr", |_, this, reg: u8| { + let ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + Ok(ctx.get_register(reg)) + }); + + methods.add_method("set_gpr", |_, this, (reg, val): (u8, u32)| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + ctx.set_register(reg, val); + Ok(()) + }); + + methods.add_method("get_fpr", |_, this, reg: u8| { + let ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + Ok(ctx.get_fpr(reg)) + }); + + methods.add_method("set_fpr", |_, this, (reg, val): (u8, f64)| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + ctx.set_fpr(reg, val); + Ok(()) + }); + + methods.add_method("get_pc", |_, this, ()| { + let ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + Ok(ctx.pc) + }); + + methods.add_method("set_pc", |_, this, val: u32| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + ctx.pc = val; + Ok(()) + }); + + methods.add_method("get_lr", |_, this, ()| { + let ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + Ok(ctx.lr) + }); + + methods.add_method("set_lr", |_, this, val: u32| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + ctx.lr = val; + Ok(()) + }); + + methods.add_method("get_cr", |_, this, ()| { + let ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + Ok(ctx.cr) + }); + + methods.add_method("get_cr_field", |_, this, field: u8| { + let ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + Ok(ctx.get_cr_field(field)) + }); + + methods.add_method("set_cr_field", |_, this, (field, val): (u8, u8)| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + ctx.set_cr_field(field, val); + Ok(()) + }); + } +} + +pub fn register(lua: &Lua, gcrecomp: &Table) -> anyhow::Result<()> { + let cpu_table = lua.create_table().into_anyhow()?; + + let new_fn = lua + .create_function(|_, ()| { + Ok(LuaCpuContext { + inner: Arc::new(Mutex::new(CpuContext::new())), + }) + }) + .into_anyhow()?; + + cpu_table.set("new", new_fn).into_anyhow()?; + gcrecomp.set("cpu", cpu_table).into_anyhow()?; + Ok(()) +} diff --git a/gcrecomp-lua/src/bindings/memory.rs b/gcrecomp-lua/src/bindings/memory.rs new file mode 100644 index 0000000..723b2f7 --- /dev/null +++ b/gcrecomp-lua/src/bindings/memory.rs @@ -0,0 +1,81 @@ +use mlua::{Lua, Table, UserData, UserDataMethods}; +use std::sync::{Arc, Mutex}; + +use gcrecomp_core::runtime::memory::MemoryManager; + +use crate::error::IntoAnyhow; + +pub struct LuaMemoryManager { + pub inner: Arc>, +} + +impl UserData for LuaMemoryManager { + fn add_methods>(methods: &mut M) { + methods.add_method("read_u8", |_, this, addr: u32| { + let mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.read_u8(addr).map_err(mlua::Error::external) + }); + + methods.add_method("read_u16", |_, this, addr: u32| { + let mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.read_u16(addr).map_err(mlua::Error::external) + }); + + methods.add_method("read_u32", |_, this, addr: u32| { + let mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.read_u32(addr).map_err(mlua::Error::external) + }); + + methods.add_method("read_u64", |_, this, addr: u32| { + let mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.read_u64(addr).map_err(mlua::Error::external) + }); + + methods.add_method("read_bytes", |_, this, (addr, len): (u32, usize)| { + let mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + let bytes = mem.read_bytes(addr, len).map_err(mlua::Error::external)?; + Ok(bytes) + }); + + methods.add_method("write_u8", |_, this, (addr, val): (u32, u8)| { + let mut mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.write_u8(addr, val).map_err(mlua::Error::external) + }); + + methods.add_method("write_u16", |_, this, (addr, val): (u32, u16)| { + let mut mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.write_u16(addr, val).map_err(mlua::Error::external) + }); + + methods.add_method("write_u32", |_, this, (addr, val): (u32, u32)| { + let mut mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.write_u32(addr, val).map_err(mlua::Error::external) + }); + + methods.add_method("write_u64", |_, this, (addr, val): (u32, u64)| { + let mut mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.write_u64(addr, val).map_err(mlua::Error::external) + }); + + methods.add_method("write_bytes", |_, this, (addr, data): (u32, Vec)| { + let mut mem = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + mem.write_bytes(addr, &data).map_err(mlua::Error::external) + }); + } +} + +pub fn register(lua: &Lua, gcrecomp: &Table) -> anyhow::Result<()> { + let memory_table = lua.create_table().into_anyhow()?; + + let new_fn = lua + .create_function(|_, ()| { + Ok(LuaMemoryManager { + inner: Arc::new(Mutex::new(MemoryManager::new())), + }) + }) + .into_anyhow()?; + + memory_table.set("new", new_fn).into_anyhow()?; + gcrecomp.set("memory", memory_table).into_anyhow()?; + Ok(()) +} diff --git a/gcrecomp-lua/src/bindings/mod.rs b/gcrecomp-lua/src/bindings/mod.rs new file mode 100644 index 0000000..9592797 --- /dev/null +++ b/gcrecomp-lua/src/bindings/mod.rs @@ -0,0 +1,26 @@ +pub mod config; +pub mod cpu; +pub mod memory; +pub mod optimize; +pub mod pipeline; +pub mod ui; +pub mod verify; + +use mlua::Lua; + +use crate::error::IntoAnyhow; + +pub fn register_all(lua: &Lua) -> anyhow::Result<()> { + let gcrecomp = lua.create_table().into_anyhow()?; + + config::register(lua, &gcrecomp)?; + pipeline::register(lua, &gcrecomp)?; + cpu::register(lua, &gcrecomp)?; + memory::register(lua, &gcrecomp)?; + ui::register(lua, &gcrecomp)?; + verify::register(lua, &gcrecomp)?; + optimize::register(lua, &gcrecomp)?; + + lua.globals().set("gcrecomp", gcrecomp).into_anyhow()?; + Ok(()) +} diff --git a/gcrecomp-lua/src/bindings/optimize.rs b/gcrecomp-lua/src/bindings/optimize.rs new file mode 100644 index 0000000..e7d493d --- /dev/null +++ b/gcrecomp-lua/src/bindings/optimize.rs @@ -0,0 +1,100 @@ +use mlua::{Lua, Table}; +use std::path::Path; + +use crate::error::IntoAnyhow; + +pub fn register(lua: &Lua, gcrecomp: &Table) -> anyhow::Result<()> { + let optimize_table = lua.create_table().into_anyhow()?; + + let dce_fn = lua + .create_function(|_, path: String| { + let code = std::fs::read_to_string(&path).map_err(mlua::Error::external)?; + let original_size = code.len(); + + // Simple dead code elimination: remove empty stub functions + let lines: Vec<&str> = code.lines().collect(); + let mut optimized = String::with_capacity(code.len()); + let mut removed = 0usize; + let mut i = 0; + + while i < lines.len() { + let line = lines[i]; + // Detect stub functions (Ok(None) body only) + if line.trim_start().starts_with("pub fn ") + && i + 2 < lines.len() + && lines[i + 1].trim() == "Ok(None)" + && lines[i + 2].trim() == "}" + { + removed += 1; + i += 3; // Skip the stub function + // Also skip trailing newline + if i < lines.len() && lines[i].is_empty() { + i += 1; + } + continue; + } + optimized.push_str(line); + optimized.push('\n'); + i += 1; + } + + std::fs::write(&path, &optimized).map_err(mlua::Error::external)?; + + Ok((original_size, optimized.len(), removed)) + }) + .into_anyhow()?; + + let strip_comments_fn = lua + .create_function(|_, path: String| { + let code = std::fs::read_to_string(&path).map_err(mlua::Error::external)?; + let original_size = code.len(); + + let optimized: String = code + .lines() + .filter(|line| { + let trimmed = line.trim(); + !trimmed.starts_with("//") || trimmed.starts_with("//!") + }) + .collect::>() + .join("\n"); + + std::fs::write(&path, &optimized).map_err(mlua::Error::external)?; + Ok((original_size, optimized.len())) + }) + .into_anyhow()?; + + let size_report_fn = lua + .create_function(|lua, path: String| { + let table = lua.create_table()?; + + if Path::new(&path).exists() { + let metadata = + std::fs::metadata(&path).map_err(mlua::Error::external)?; + let size = metadata.len(); + table.set("size_bytes", size)?; + table.set("size_kb", size as f64 / 1024.0)?; + table.set("size_mb", size as f64 / (1024.0 * 1024.0))?; + + // Count functions + let code = std::fs::read_to_string(&path).map_err(mlua::Error::external)?; + let fn_count = code.matches("pub fn ").count(); + let line_count = code.lines().count(); + table.set("functions", fn_count)?; + table.set("lines", line_count)?; + } else { + table.set("error", "File not found")?; + } + Ok(table) + }) + .into_anyhow()?; + + optimize_table.set("dce", dce_fn).into_anyhow()?; + optimize_table + .set("strip_comments", strip_comments_fn) + .into_anyhow()?; + optimize_table + .set("size_report", size_report_fn) + .into_anyhow()?; + gcrecomp.set("optimize", optimize_table).into_anyhow()?; + Ok(()) +} diff --git a/gcrecomp-lua/src/bindings/pipeline.rs b/gcrecomp-lua/src/bindings/pipeline.rs new file mode 100644 index 0000000..a8e61b0 --- /dev/null +++ b/gcrecomp-lua/src/bindings/pipeline.rs @@ -0,0 +1,103 @@ +use mlua::{Lua, Table, UserData, UserDataMethods}; +use std::sync::{Arc, Mutex}; + +use gcrecomp_core::recompiler::pipeline::{PipelineContext, RecompilationPipeline}; + +use crate::error::IntoAnyhow; + +struct LuaPipelineContext { + inner: Arc>, +} + +impl UserData for LuaPipelineContext { + fn add_methods>(methods: &mut M) { + methods.add_method("load_dol", |_, this, path: String| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_load_dol(&mut ctx, &path) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("analyze", |_, this, ()| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_analyze(&mut ctx) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("decode", |_, this, ()| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_decode(&mut ctx) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("build_cfg", |_, this, ()| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_build_cfg(&mut ctx) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("analyze_data_flow", |_, this, ()| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_analyze_data_flow(&mut ctx) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("infer_types", |_, this, ()| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_infer_types(&mut ctx) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("generate_code", |_, this, ()| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_generate_code(&mut ctx) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("validate", |_, this, ()| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_validate(&mut ctx) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("write_output", |_, this, path: String| { + let mut ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + RecompilationPipeline::stage_write_output(&mut ctx, &path) + .map_err(mlua::Error::external)?; + Ok(()) + }); + + methods.add_method("get_stats", |lua, this, ()| { + let ctx = this.inner.lock().map_err(|e| mlua::Error::external(e.to_string()))?; + let table = lua.create_table()?; + table.set("total_functions", ctx.stats.total_functions)?; + table.set("successful_functions", ctx.stats.successful_functions)?; + table.set("failed_functions", ctx.stats.failed_functions)?; + table.set("total_instructions", ctx.stats.total_instructions)?; + Ok(table) + }); + } +} + +pub fn register(lua: &Lua, gcrecomp: &Table) -> anyhow::Result<()> { + let pipeline_table = lua.create_table().into_anyhow()?; + + let new_context_fn = lua + .create_function(|_, ()| { + Ok(LuaPipelineContext { + inner: Arc::new(Mutex::new(PipelineContext::new())), + }) + }) + .into_anyhow()?; + + pipeline_table.set("new_context", new_context_fn).into_anyhow()?; + gcrecomp.set("pipeline", pipeline_table).into_anyhow()?; + Ok(()) +} diff --git a/gcrecomp-lua/src/bindings/ui.rs b/gcrecomp-lua/src/bindings/ui.rs new file mode 100644 index 0000000..482c721 --- /dev/null +++ b/gcrecomp-lua/src/bindings/ui.rs @@ -0,0 +1,99 @@ +use mlua::{Lua, Table}; +use std::sync::{Arc, Mutex}; + +use crate::error::IntoAnyhow; + +/// A Lua-defined screen widget. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct LuaWidget { + #[serde(rename = "type")] + pub widget_type: String, + pub text: Option, + pub label: Option, + pub value: Option, + pub min: Option, + pub max: Option, + pub options: Option>, +} + +/// A Lua-defined screen definition. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct LuaScreenDef { + pub id: String, + pub title: String, + pub widgets: Vec, +} + +/// Global registry of Lua-defined screens. +pub static LUA_SCREENS: std::sync::LazyLock>>> = + std::sync::LazyLock::new(|| Arc::new(Mutex::new(Vec::new()))); + +pub fn register(lua: &Lua, gcrecomp: &Table) -> anyhow::Result<()> { + let ui_table = lua.create_table().into_anyhow()?; + + let register_screen_fn = lua + .create_function(|_, (id, def): (String, Table)| { + let title: String = def.get("title")?; + let widgets_table: Table = def.get("widgets")?; + + let mut widgets = Vec::new(); + for i in 1..=widgets_table.raw_len() { + let w: Table = widgets_table.get(i)?; + let widget = LuaWidget { + widget_type: w.get("type")?, + text: w.get("text").ok(), + label: w.get("label").ok(), + value: None, + min: w.get("min").ok(), + max: w.get("max").ok(), + options: None, + }; + widgets.push(widget); + } + + let screen_def = LuaScreenDef { + id: id.clone(), + title, + widgets, + }; + + let mut screens = LUA_SCREENS + .lock() + .map_err(|e| mlua::Error::external(e.to_string()))?; + + // Replace existing screen with same id, or add new + if let Some(existing) = screens.iter_mut().find(|s| s.id == id) { + *existing = screen_def; + } else { + screens.push(screen_def); + } + + Ok(()) + }) + .into_anyhow()?; + + let list_screens_fn = lua + .create_function(|lua, ()| { + let screens = LUA_SCREENS + .lock() + .map_err(|e| mlua::Error::external(e.to_string()))?; + let table = lua.create_table()?; + for (i, screen) in screens.iter().enumerate() { + let s = lua.create_table()?; + s.set("id", screen.id.as_str())?; + s.set("title", screen.title.as_str())?; + table.set(i + 1, s)?; + } + Ok(table) + }) + .into_anyhow()?; + + ui_table + .set("register_screen", register_screen_fn) + .into_anyhow()?; + ui_table + .set("list_screens", list_screens_fn) + .into_anyhow()?; + gcrecomp.set("ui", ui_table).into_anyhow()?; + Ok(()) +} diff --git a/gcrecomp-lua/src/bindings/verify.rs b/gcrecomp-lua/src/bindings/verify.rs new file mode 100644 index 0000000..8ab9bfe --- /dev/null +++ b/gcrecomp-lua/src/bindings/verify.rs @@ -0,0 +1,92 @@ +use mlua::{Lua, Table}; +use sha2::{Digest, Sha256}; +use std::path::Path; + +use crate::error::IntoAnyhow; + +pub fn register(lua: &Lua, gcrecomp: &Table) -> anyhow::Result<()> { + let verify_table = lua.create_table().into_anyhow()?; + + let crc32_fn = lua + .create_function(|_, path: String| { + let data = std::fs::read(&path).map_err(mlua::Error::external)?; + let crc = crc32fast::hash(&data); + Ok(format!("{:08X}", crc)) + }) + .into_anyhow()?; + + let sha256_fn = lua + .create_function(|_, path: String| { + let data = std::fs::read(&path).map_err(mlua::Error::external)?; + let mut hasher = Sha256::new(); + hasher.update(&data); + let result = hasher.finalize(); + Ok(format!("{:x}", result)) + }) + .into_anyhow()?; + + let check_compiles_fn = lua + .create_function(|_, path: String| { + // Check if the file is valid Rust by looking for balanced braces and fn definitions + let code = std::fs::read_to_string(&path).map_err(mlua::Error::external)?; + let opens = code.matches('{').count(); + let closes = code.matches('}').count(); + let has_fns = code.contains("fn "); + Ok(opens == closes && has_fns) + }) + .into_anyhow()?; + + let smoke_test_fn = lua + .create_function(|lua, (binary_path, _timeout_ms): (String, u64)| { + let result = lua.create_table()?; + if !Path::new(&binary_path).exists() { + result.set("success", false)?; + result.set("error", lua.create_string("Binary not found")?)?; + return Ok(result); + } + + let output = std::process::Command::new(&binary_path) + .arg("--smoke-test") + .output(); + + match output { + Ok(out) => { + result.set("success", out.status.success())?; + result.set("exit_code", out.status.code().unwrap_or(-1))?; + result.set( + "stdout", + lua.create_string(String::from_utf8_lossy(&out.stdout).as_bytes())?, + )?; + result.set( + "stderr", + lua.create_string(String::from_utf8_lossy(&out.stderr).as_bytes())?, + )?; + } + Err(e) => { + result.set("success", false)?; + result.set("error", lua.create_string(e.to_string().as_bytes())?)?; + } + } + Ok(result) + }) + .into_anyhow()?; + + let file_size_fn = lua + .create_function(|_, path: String| { + let metadata = std::fs::metadata(&path).map_err(mlua::Error::external)?; + Ok(metadata.len()) + }) + .into_anyhow()?; + + verify_table.set("crc32", crc32_fn).into_anyhow()?; + verify_table.set("sha256", sha256_fn).into_anyhow()?; + verify_table + .set("check_compiles", check_compiles_fn) + .into_anyhow()?; + verify_table + .set("smoke_test", smoke_test_fn) + .into_anyhow()?; + verify_table.set("file_size", file_size_fn).into_anyhow()?; + gcrecomp.set("verify", verify_table).into_anyhow()?; + Ok(()) +} diff --git a/gcrecomp-lua/src/engine.rs b/gcrecomp-lua/src/engine.rs new file mode 100644 index 0000000..a82cc8a --- /dev/null +++ b/gcrecomp-lua/src/engine.rs @@ -0,0 +1,45 @@ +use anyhow::Context; +use mlua::Lua; +use std::path::Path; + +use crate::bindings; +use crate::error::IntoAnyhow; + +pub struct LuaEngine { + lua: Lua, +} + +impl LuaEngine { + pub fn new() -> anyhow::Result { + let lua = Lua::new(); + + bindings::register_all(&lua)?; + + Ok(Self { lua }) + } + + pub fn execute_file(&self, path: &Path) -> anyhow::Result<()> { + let script = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read Lua script: {}", path.display()))?; + self.lua + .load(&script) + .set_name(path.to_string_lossy()) + .exec() + .into_anyhow() + .with_context(|| format!("Failed to execute Lua script: {}", path.display()))?; + Ok(()) + } + + pub fn execute_string(&self, code: &str) -> anyhow::Result<()> { + self.lua + .load(code) + .exec() + .into_anyhow() + .context("Failed to execute Lua string")?; + Ok(()) + } + + pub fn lua(&self) -> &Lua { + &self.lua + } +} diff --git a/gcrecomp-lua/src/error.rs b/gcrecomp-lua/src/error.rs new file mode 100644 index 0000000..65f67ba --- /dev/null +++ b/gcrecomp-lua/src/error.rs @@ -0,0 +1,38 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum LuaBindingError { + #[error("Lua runtime error: {0}")] + Runtime(String), + + #[error("Binding error: {0}")] + Binding(String), + + #[error("Script not found: {0}")] + ScriptNotFound(String), + + #[error("Serialization error: {0}")] + Serialization(String), +} + +impl From for LuaBindingError { + fn from(err: mlua::Error) -> Self { + LuaBindingError::Runtime(err.to_string()) + } +} + +impl From for mlua::Error { + fn from(err: LuaBindingError) -> Self { + mlua::Error::external(err) + } +} + +pub trait IntoAnyhow { + fn into_anyhow(self) -> anyhow::Result; +} + +impl IntoAnyhow for Result { + fn into_anyhow(self) -> anyhow::Result { + self.map_err(|e| anyhow::anyhow!("{}", e)) + } +} diff --git a/gcrecomp-lua/src/lib.rs b/gcrecomp-lua/src/lib.rs new file mode 100644 index 0000000..1f5fcf7 --- /dev/null +++ b/gcrecomp-lua/src/lib.rs @@ -0,0 +1,3 @@ +pub mod engine; +pub mod error; +pub mod bindings; diff --git a/gcrecomp-ui/src/app.rs b/gcrecomp-ui/src/app.rs index ae93ae0..da0f991 100644 --- a/gcrecomp-ui/src/app.rs +++ b/gcrecomp-ui/src/app.rs @@ -17,6 +17,7 @@ pub enum Message { OpenGameSettings, CloseMenu, ConfigChanged(GameConfig), + OpenLuaScreen(String), } pub struct App { @@ -25,7 +26,7 @@ pub struct App { config: GameConfig, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] enum Screen { MainMenu, FpsSettings, @@ -34,6 +35,7 @@ enum Screen { InputSettings, GameSettings, ControllerConfig, + LuaScreen(String), } impl Application for App { @@ -91,6 +93,9 @@ impl Application for App { eprintln!("Failed to save config: {}", e); } } + Message::OpenLuaScreen(id) => { + self.current_screen = Screen::LuaScreen(id); + } } Command::none() } @@ -117,6 +122,13 @@ impl Application for App { crate::ui::controller_config::ControllerConfigUI::view(&self.config) } Screen::GameSettings => crate::ui::game_settings::GameSettings::view(&self.config), + Screen::LuaScreen(ref id) => { + // Render a placeholder for Lua-defined screens + iced::widget::Column::new() + .push(Text::new(format!("Lua Screen: {}", id))) + .push(Text::new("(Lua-defined content rendered here)")) + .into() + } }; Container::new(content) diff --git a/gcrecomp-ui/src/integration.rs b/gcrecomp-ui/src/integration.rs index 8347a7c..494df93 100644 --- a/gcrecomp-ui/src/integration.rs +++ b/gcrecomp-ui/src/integration.rs @@ -6,15 +6,21 @@ use winit::window::Window; pub struct GameIntegration { menu_visible: bool, + lua_event_handler: Option bool + Send>>, } impl GameIntegration { pub fn new() -> Self { Self { menu_visible: false, + lua_event_handler: None, } } + pub fn set_lua_event_handler(&mut self, handler: Box bool + Send>) { + self.lua_event_handler = Some(handler); + } + pub fn handle_event(&mut self, event: &WindowEvent) -> bool { match event { WindowEvent::KeyboardInput { event, .. } => { @@ -25,6 +31,12 @@ impl GameIntegration { return true; // Event handled, don't pass to game } if self.menu_visible { + // Try Lua handler first + if let Some(ref handler) = self.lua_event_handler { + if handler("keyboard") { + return true; + } + } return true; // Consume keyboard input when menu is visible } false diff --git a/gcrecomp-web/Cargo.toml b/gcrecomp-web/Cargo.toml new file mode 100644 index 0000000..6bdf914 --- /dev/null +++ b/gcrecomp-web/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "gcrecomp-web" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Web dashboard for GameCube static recompiler" + +[[bin]] +name = "gcrecomp-web" +path = "src/main.rs" + +[dependencies] +gcrecomp-core = { path = "../gcrecomp-core" } +gcrecomp-lua = { path = "../gcrecomp-lua" } +gcrecomp-ui = { path = "../gcrecomp-ui" } +axum = { workspace = true } +tokio = { workspace = true } +tower-http = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +log = { workspace = true } +env_logger = "0.11" diff --git a/gcrecomp-web/src/main.rs b/gcrecomp-web/src/main.rs new file mode 100644 index 0000000..43604e3 --- /dev/null +++ b/gcrecomp-web/src/main.rs @@ -0,0 +1,12 @@ +mod routes; +mod security; +mod server; + +use anyhow::Result; + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + let server = server::WebServer::new()?; + server.run().await +} diff --git a/gcrecomp-web/src/routes.rs b/gcrecomp-web/src/routes.rs new file mode 100644 index 0000000..d3f39fc --- /dev/null +++ b/gcrecomp-web/src/routes.rs @@ -0,0 +1,207 @@ +use axum::{ + extract::{Multipart, State}, + routing::{get, post, put}, + Json, Router, +}; +use gcrecomp_core::recompiler::pipeline::{PipelineContext, RecompilationPipeline}; +use std::sync::Arc; + +use crate::security; +use crate::server::{AppState, RecompileStatus}; + +pub fn api_routes() -> Router> { + Router::new() + .route("/upload", post(upload_dol)) + .route("/recompile", post(start_recompile)) + .route("/status", get(get_status)) + .route("/config", get(get_config)) + .route("/config", put(update_config)) + .route("/targets", get(list_targets)) +} + +async fn upload_dol( + State(state): State>, + mut multipart: Multipart, +) -> Result, (axum::http::StatusCode, String)> { + while let Some(field) = multipart + .next_field() + .await + .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, e.to_string()))? + { + let data = field + .bytes() + .await + .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, e.to_string()))?; + + if data.len() > security::MAX_UPLOAD_SIZE { + return Err(( + axum::http::StatusCode::PAYLOAD_TOO_LARGE, + "File too large".to_string(), + )); + } + + if !security::validate_dol_magic(&data) { + return Err(( + axum::http::StatusCode::BAD_REQUEST, + "Invalid DOL file".to_string(), + )); + } + + // Save to temp location + let upload_dir = std::path::Path::new("uploads"); + std::fs::create_dir_all(upload_dir) + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let path = upload_dir.join("uploaded.dol"); + std::fs::write(&path, &data) + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + // Initialize pipeline context + let mut ctx = PipelineContext::new(); + let dol = gcrecomp_core::recompiler::parser::DolFile::parse( + &data, + path.to_str().unwrap_or("uploaded.dol"), + ) + .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, e.to_string()))?; + ctx.dol_file = Some(dol); + + *state.pipeline_ctx.lock().await = Some(ctx); + + return Ok(Json(serde_json::json!({ + "status": "uploaded", + "size": data.len(), + }))); + } + + Err(( + axum::http::StatusCode::BAD_REQUEST, + "No file provided".to_string(), + )) +} + +async fn start_recompile( + State(state): State>, +) -> Result, (axum::http::StatusCode, String)> { + // Check that we have a pipeline context + { + let ctx = state.pipeline_ctx.lock().await; + if ctx.is_none() { + return Err(( + axum::http::StatusCode::BAD_REQUEST, + "No DOL file uploaded".to_string(), + )); + } + } + + // Update status + { + let mut status = state.current_status.lock().await; + *status = RecompileStatus { + state: "running".to_string(), + stage: "analyze".to_string(), + stats: None, + error: None, + }; + } + + // Run pipeline stages in a background task + let state_clone = Arc::clone(&state); + tokio::spawn(async move { + let stages: &[(&str, fn(&mut PipelineContext) -> anyhow::Result<()>)] = &[ + ("analyze", RecompilationPipeline::stage_analyze), + ("decode", RecompilationPipeline::stage_decode), + ("build_cfg", RecompilationPipeline::stage_build_cfg), + ("data_flow", RecompilationPipeline::stage_analyze_data_flow), + ("type_inference", RecompilationPipeline::stage_infer_types), + ("codegen", RecompilationPipeline::stage_generate_code), + ("validate", RecompilationPipeline::stage_validate), + ]; + + for (name, stage_fn) in stages { + { + let mut status = state_clone.current_status.lock().await; + status.stage = name.to_string(); + } + + let result = { + let mut ctx_guard = state_clone.pipeline_ctx.lock().await; + if let Some(ref mut ctx) = *ctx_guard { + stage_fn(ctx) + } else { + Err(anyhow::anyhow!("Pipeline context lost")) + } + }; + + if let Err(e) = result { + let mut status = state_clone.current_status.lock().await; + status.state = "error".to_string(); + status.error = Some(e.to_string()); + return; + } + } + + // Write output + { + let mut ctx_guard = state_clone.pipeline_ctx.lock().await; + if let Some(ref mut ctx) = *ctx_guard { + std::fs::create_dir_all("output").ok(); + if let Err(e) = RecompilationPipeline::stage_write_output(ctx, "output/recompiled.rs") { + let mut status = state_clone.current_status.lock().await; + status.state = "error".to_string(); + status.error = Some(e.to_string()); + return; + } + + let mut status = state_clone.current_status.lock().await; + status.state = "complete".to_string(); + status.stage = "done".to_string(); + status.stats = Some(ctx.stats.clone()); + } + } + }); + + Ok(Json(serde_json::json!({ + "status": "started", + }))) +} + +async fn get_status( + State(state): State>, +) -> Json { + let status = state.current_status.lock().await; + Json(serde_json::json!({ + "state": status.state, + "stage": status.stage, + "stats": status.stats, + "error": status.error, + })) +} + +async fn get_config() -> Result, (axum::http::StatusCode, String)> { + let config = gcrecomp_ui::config::GameConfig::load() + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let value = serde_json::to_value(&config) + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + Ok(Json(value)) +} + +async fn update_config( + Json(value): Json, +) -> Result, (axum::http::StatusCode, String)> { + let config: gcrecomp_ui::config::GameConfig = serde_json::from_value(value) + .map_err(|e| (axum::http::StatusCode::BAD_REQUEST, e.to_string()))?; + config + .save() + .map_err(|e| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + Ok(Json(serde_json::json!({"status": "saved"}))) +} + +async fn list_targets() -> Json { + Json(serde_json::json!({ + "targets": [ + {"id": "x86_64-linux", "name": "x86_64 Linux"}, + {"id": "x86_64-windows", "name": "x86_64 Windows"}, + {"id": "aarch64-linux", "name": "AArch64 Linux"}, + {"id": "aarch64-macos", "name": "AArch64 macOS"}, + ] + })) +} diff --git a/gcrecomp-web/src/security.rs b/gcrecomp-web/src/security.rs new file mode 100644 index 0000000..8f55e18 --- /dev/null +++ b/gcrecomp-web/src/security.rs @@ -0,0 +1,34 @@ +use std::net::SocketAddr; +use std::path::Path; + +/// Bind to localhost only for security. +pub fn bind_address() -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], 8080)) +} + +/// Maximum upload size: 64 MB (largest GameCube DOL/ISO sections). +pub const MAX_UPLOAD_SIZE: usize = 64 * 1024 * 1024; + +/// Validate that the uploaded file looks like a DOL. +pub fn validate_dol_magic(data: &[u8]) -> bool { + // DOL files have specific section header layout starting at offset 0 + // Text section offsets start at 0x00, data section offsets at 0x1C + // A valid DOL should be at least 0x100 bytes (header size) + if data.len() < 0x100 { + return false; + } + // Check that the first text section offset is reasonable (non-zero, aligned) + let first_offset = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); + first_offset >= 0x100 && first_offset % 4 == 0 +} + +/// Sanitize output path to prevent directory traversal. +#[allow(dead_code)] +pub fn sanitize_path(path: &str) -> Option<&str> { + let p = Path::new(path); + // Reject absolute paths and path traversal + if p.is_absolute() || path.contains("..") { + return None; + } + Some(path) +} diff --git a/gcrecomp-web/src/server.rs b/gcrecomp-web/src/server.rs new file mode 100644 index 0000000..351cac5 --- /dev/null +++ b/gcrecomp-web/src/server.rs @@ -0,0 +1,66 @@ +use anyhow::Result; +use axum::Router; +use std::sync::Arc; +use tokio::sync::Mutex; +use tower_http::services::ServeDir; + +use gcrecomp_core::recompiler::pipeline::{PipelineContext, PipelineStats}; +use gcrecomp_lua::engine::LuaEngine; + +use crate::routes; +use crate::security; + +pub struct AppState { + #[allow(dead_code)] + pub lua_engine: Mutex, + pub pipeline_ctx: Mutex>, + pub current_status: Mutex, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct RecompileStatus { + pub state: String, + pub stage: String, + pub stats: Option, + pub error: Option, +} + +impl Default for RecompileStatus { + fn default() -> Self { + Self { + state: "idle".to_string(), + stage: "".to_string(), + stats: None, + error: None, + } + } +} + +pub struct WebServer { + state: Arc, +} + +impl WebServer { + pub fn new() -> Result { + let lua_engine = LuaEngine::new()?; + let state = Arc::new(AppState { + lua_engine: Mutex::new(lua_engine), + pipeline_ctx: Mutex::new(None), + current_status: Mutex::new(RecompileStatus::default()), + }); + Ok(Self { state }) + } + + pub async fn run(self) -> Result<()> { + let app = Router::new() + .nest("/api", routes::api_routes()) + .fallback_service(ServeDir::new("web/static")) + .with_state(self.state); + + let addr = security::bind_address(); + let listener = tokio::net::TcpListener::bind(addr).await?; + log::info!("Web UI server running at http://{}", addr); + axum::serve(listener, app).await?; + Ok(()) + } +} diff --git a/lua/game/init.lua b/lua/game/init.lua new file mode 100644 index 0000000..9467983 --- /dev/null +++ b/lua/game/init.lua @@ -0,0 +1,10 @@ +-- Game initialization script +-- Loaded at game startup to register Lua-defined menus and handlers + +print("[game] Lua game scripts loading...") + +-- Load menu definitions +local menus = require("lua.game.menus") + +print("[game] Lua game scripts loaded successfully") +print("[game] Registered " .. #menus .. " menu screens") diff --git a/lua/game/menus.lua b/lua/game/menus.lua new file mode 100644 index 0000000..52c43db --- /dev/null +++ b/lua/game/menus.lua @@ -0,0 +1,28 @@ +-- Default menu definitions for in-game overlay +-- Users can modify this file to customize menus + +local menus = {} + +-- FPS settings screen +gcrecomp.ui.register_screen("lua_fps", { + title = "FPS Settings (Lua)", + widgets = { + { type = "button", text = "30 FPS" }, + { type = "button", text = "60 FPS" }, + { type = "button", text = "Unlimited" }, + }, +}) +table.insert(menus, "lua_fps") + +-- Quick settings screen +gcrecomp.ui.register_screen("lua_quick", { + title = "Quick Settings", + widgets = { + { type = "button", text = "Toggle VSync" }, + { type = "button", text = "Toggle Widescreen" }, + { type = "button", text = "Reset to Defaults" }, + }, +}) +table.insert(menus, "lua_quick") + +return menus diff --git a/lua/init.lua b/lua/init.lua new file mode 100644 index 0000000..e279bd3 --- /dev/null +++ b/lua/init.lua @@ -0,0 +1,19 @@ +-- GCRecomp Lua entry point +-- Validates that all bindings are available and functional + +print("[gcrecomp] Lua scripting engine initialized") + +-- Validate config bindings +assert(gcrecomp ~= nil, "gcrecomp global table missing") +assert(gcrecomp.config ~= nil, "gcrecomp.config missing") +assert(type(gcrecomp.config.load) == "function", "gcrecomp.config.load is not a function") +assert(type(gcrecomp.config.save) == "function", "gcrecomp.config.save is not a function") + +-- Round-trip test: load config, modify, save, reload, verify +local config = gcrecomp.config.load() +print("[gcrecomp] Config loaded successfully") +print("[gcrecomp] fps_limit = " .. tostring(config.fps_limit)) +print("[gcrecomp] vsync = " .. tostring(config.vsync)) +print("[gcrecomp] render_scale = " .. tostring(config.render_scale)) + +print("[gcrecomp] All bindings validated successfully") diff --git a/lua/optimize/default.lua b/lua/optimize/default.lua new file mode 100644 index 0000000..2426777 --- /dev/null +++ b/lua/optimize/default.lua @@ -0,0 +1,43 @@ +-- Default optimization pipeline for recompiled output +-- Applies dead code elimination and size reporting + +local function optimize(output_path) + print("[optimize] Starting optimization pipeline") + print("[optimize] Input: " .. output_path) + + -- Pre-optimization size report + local before = gcrecomp.optimize.size_report(output_path) + print(string.format("[optimize] Before: %.1f KB, %d functions, %d lines", + before.size_kb, before.functions, before.lines)) + + -- Dead code elimination + local orig, after_dce, removed = gcrecomp.optimize.dce(output_path) + print(string.format("[optimize] DCE: removed %d stub functions (%.1f KB -> %.1f KB)", + removed, orig / 1024, after_dce / 1024)) + + -- Strip comments + local before_strip, after_strip = gcrecomp.optimize.strip_comments(output_path) + print(string.format("[optimize] Strip comments: %.1f KB -> %.1f KB", + before_strip / 1024, after_strip / 1024)) + + -- Post-optimization size report + local final_report = gcrecomp.optimize.size_report(output_path) + print(string.format("[optimize] After: %.1f KB, %d functions, %d lines", + final_report.size_kb, final_report.functions, final_report.lines)) + + local reduction = 0 + if before.size_bytes > 0 then + reduction = (1.0 - final_report.size_bytes / before.size_bytes) * 100 + end + print(string.format("[optimize] Size reduction: %.1f%%", reduction)) + + return { + before = before, + after = final_report, + reduction_percent = reduction, + } +end + +return { + optimize = optimize, +} diff --git a/lua/pipeline.lua b/lua/pipeline.lua new file mode 100644 index 0000000..6f4e9e7 --- /dev/null +++ b/lua/pipeline.lua @@ -0,0 +1,50 @@ +-- GCRecomp default pipeline orchestration script +-- Users can customize this script to modify the recompilation pipeline + +local function run_pipeline(dol_path, output_path) + print("[pipeline] Starting recompilation pipeline") + print("[pipeline] Input: " .. dol_path) + print("[pipeline] Output: " .. output_path) + + local ctx = gcrecomp.pipeline.new_context() + + print("[pipeline] Loading DOL file...") + ctx:load_dol(dol_path) + + print("[pipeline] Running Ghidra analysis...") + ctx:analyze() + + print("[pipeline] Decoding instructions...") + ctx:decode() + + print("[pipeline] Building control flow graph...") + ctx:build_cfg() + + print("[pipeline] Analyzing data flow...") + ctx:analyze_data_flow() + + print("[pipeline] Inferring types...") + ctx:infer_types() + + print("[pipeline] Generating code...") + ctx:generate_code() + + print("[pipeline] Validating output...") + ctx:validate() + + print("[pipeline] Writing output...") + ctx:write_output(output_path) + + local stats = ctx:get_stats() + print("[pipeline] Recompilation complete!") + print("[pipeline] Total functions: " .. stats.total_functions) + print("[pipeline] Successful: " .. stats.successful_functions) + print("[pipeline] Failed: " .. stats.failed_functions) + print("[pipeline] Total instructions: " .. stats.total_instructions) + + return stats +end + +return { + run = run_pipeline, +} diff --git a/lua/verify/default.lua b/lua/verify/default.lua new file mode 100644 index 0000000..b2b8d16 --- /dev/null +++ b/lua/verify/default.lua @@ -0,0 +1,56 @@ +-- Default verification suite for recompiled output +-- Runs a series of checks to validate recompilation quality + +local function verify(output_path, binary_path) + local results = { + passed = 0, + failed = 0, + checks = {}, + } + + local function check(name, ok, detail) + table.insert(results.checks, { + name = name, + passed = ok, + detail = detail or "", + }) + if ok then + results.passed = results.passed + 1 + print("[verify] PASS: " .. name) + else + results.failed = results.failed + 1 + print("[verify] FAIL: " .. name .. " - " .. (detail or "")) + end + end + + -- Check 1: Output file exists and is valid Rust + if output_path then + local compiles = gcrecomp.verify.check_compiles(output_path) + check("Syntax check", compiles, "Balanced braces and function definitions") + + -- Check 2: File size is reasonable + local size = gcrecomp.verify.file_size(output_path) + check("File size > 0", size > 0, "Size: " .. size .. " bytes") + + -- Check 3: CRC32 checksum (for reproducibility tracking) + local crc = gcrecomp.verify.crc32(output_path) + check("CRC32 computed", crc ~= nil and #crc == 8, "CRC32: " .. (crc or "nil")) + + -- Check 4: SHA256 checksum + local sha = gcrecomp.verify.sha256(output_path) + check("SHA256 computed", sha ~= nil and #sha == 64, "SHA256: " .. (sha or "nil"):sub(1, 16) .. "...") + end + + -- Check 5: Smoke test (if binary available) + if binary_path then + local result = gcrecomp.verify.smoke_test(binary_path, 10000) + check("Smoke test", result.success, result.error or ("Exit code: " .. tostring(result.exit_code))) + end + + print(string.format("\n[verify] Results: %d passed, %d failed", results.passed, results.failed)) + return results +end + +return { + verify = verify, +} diff --git a/lua/web/routes.lua b/lua/web/routes.lua new file mode 100644 index 0000000..f651475 --- /dev/null +++ b/lua/web/routes.lua @@ -0,0 +1,33 @@ +-- Web route handlers for GCRecomp dashboard +-- These handlers are called by the Rust web server to process API requests + +local routes = {} + +function routes.handle_recompile(params) + local dol_path = params.dol_path or "uploads/uploaded.dol" + local output_path = params.output_path or "output/recompiled.rs" + + local ctx = gcrecomp.pipeline.new_context() + ctx:load_dol(dol_path) + ctx:analyze() + ctx:decode() + ctx:build_cfg() + ctx:analyze_data_flow() + ctx:infer_types() + ctx:generate_code() + ctx:validate() + ctx:write_output(output_path) + + return ctx:get_stats() +end + +function routes.handle_config_get() + return gcrecomp.config.load() +end + +function routes.handle_config_set(config) + gcrecomp.config.save(config) + return { status = "saved" } +end + +return routes diff --git a/web/static/index.html b/web/static/index.html new file mode 100644 index 0000000..d90a9a1 --- /dev/null +++ b/web/static/index.html @@ -0,0 +1,143 @@ + + + + + + GCRecomp Dashboard + + + +
+

GCRecomp Dashboard

+

GameCube Static Recompiler

+ +
+

Upload DOL File

+ + + +
+
+ +
+

Recompile

+ +
+
Idle
+ +
+
+ +
+

Configuration

+
Loading...
+
+
+ + + +