2026-07-05 01:18:33 +01:00

148 lines
4.5 KiB
Rust

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<u8> {
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<u8> {
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<u8> {
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"]));
}
}