use anyhow::Result; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, DebugInfo, Export, FunctionMeta, SyscallDecl}; use prometeu_hal::color::Color; use std::fs; use std::path::PathBuf; const APP_ID: u32 = 2; const TITLE: &str = "Dummy Boy"; const APP_VERSION: &str = "0.1.0"; fn asm(s: &str) -> Vec { assemble(s).expect("assemble") } pub fn generate() -> Result<()> { let out_dir = cartridge_dir(); fs::create_dir_all(&out_dir)?; fs::write(out_dir.join("program.pbx"), build_program())?; fs::write(out_dir.join("manifest.json"), manifest_json())?; Ok(()) } pub fn build_program() -> Vec { let mut rom = Vec::new(); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); rom.extend(asm(&format!("PUSH_I64 {}\nHOSTCALL 0", Color::BLACK.raw()))); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMOD\nPUSH_I32 0\nEQ")); let jump_to_green_case_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::RED.raw()))); let jump_to_draw_after_red_offset = rom.len() + 2; rom.extend(asm("JMP 0")); let green_case = rom.len() as u32; rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMOD\nPUSH_I32 1\nEQ")); let jump_to_blue_case_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::GREEN.raw()))); let jump_to_draw_after_green_offset = rom.len() + 2; rom.extend(asm("JMP 0")); let blue_case = rom.len() as u32; rom.extend(asm(&format!("PUSH_I64 {}\nSET_LOCAL 0", Color::BLUE.raw()))); let draw = rom.len() as u32; rom.extend(asm(&format!( "PUSH_I32 180\n\ PUSH_I32 75\n\ PUSH_I32 120\n\ PUSH_I32 120\n\ PUSH_I64 {}\n\ GET_LOCAL 0\n\ HOSTCALL 1\n\ FRAME_SYNC\n\ RET", Color::WHITE.raw() ))); patch_jump(&mut rom, jump_to_green_case_offset, green_case); patch_jump(&mut rom, jump_to_draw_after_red_offset, draw); patch_jump(&mut rom, jump_to_blue_case_offset, blue_case); patch_jump(&mut rom, jump_to_draw_after_green_offset, draw); BytecodeModule { version: 0, const_pool: vec![], functions: vec![FunctionMeta { code_offset: 0, code_len: rom.len() as u32, param_slots: 0, local_slots: 1, return_slots: 0, max_stack_slots: 8, }], code: rom, debug_info: Some(DebugInfo { pc_to_span: vec![], function_names: vec![(0, "main".into())], }), exports: vec![Export { symbol: "main".into(), func_idx: 0 }], syscalls: vec![ SyscallDecl { module: "gfx2d".into(), name: "clear".into(), version: 1, arg_slots: 1, ret_slots: 0, }, SyscallDecl { module: "gfx2d".into(), name: "draw_square".into(), version: 1, arg_slots: 6, ret_slots: 0, }, ], } .serialize() } pub fn manifest_json() -> Vec { format!( "{{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": {APP_ID},\n \"title\": \"{TITLE}\",\n \"app_version\": \"{APP_VERSION}\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx2d\"]\n}}\n" ) .into_bytes() } fn cartridge_dir() -> PathBuf { let mut out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); out_dir.pop(); out_dir.pop(); out_dir.pop(); out_dir.push("test-cartridges"); out_dir.push("dummy-boy"); out_dir } fn patch_jump(buf: &mut [u8], imm_offset: usize, target: u32) { buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes()); } #[cfg(test)] mod tests { use super::*; #[test] fn generated_program_is_pbx_module_with_expected_syscalls() { let bytes = build_program(); assert!(bytes.starts_with(b"PBS\0")); assert!(bytes.len() > 64); } #[test] fn manifest_declares_distinct_game_identity() { let manifest: serde_json::Value = serde_json::from_slice(&manifest_json()).expect("manifest should parse"); assert_eq!(manifest["app_id"], APP_ID); assert_eq!(manifest["title"], TITLE); assert_eq!(manifest["app_mode"], "Game"); assert_eq!(manifest["capabilities"], serde_json::json!(["gfx2d"])); } }