912 lines
36 KiB
Rust
912 lines
36 KiB
Rust
use super::*;
|
|
use crate::fs::{FsState, VirtualFS};
|
|
use crate::services::memcard::{MemcardService, MemcardSlotState, MemcardStatus};
|
|
use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
|
|
use prometeu_hal::app_mode::AppMode;
|
|
use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
|
|
use prometeu_hal::color::Color;
|
|
use prometeu_hal::glyph::Glyph;
|
|
use prometeu_hal::log::{LogLevel, LogSource};
|
|
use prometeu_hal::primitives::Rect;
|
|
use prometeu_hal::sprite::Sprite;
|
|
use prometeu_hal::syscalls::Syscall;
|
|
use prometeu_hal::vm_fault::VmFault;
|
|
use prometeu_hal::{
|
|
AudioOpStatus, ComposerOpStatus, Gfx2dCommand, GfxUiCommand, HostContext, HostReturn,
|
|
NativeInterface, SyscallId, expect_bool, expect_int,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::ops::{Deref, DerefMut};
|
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
use std::sync::atomic::Ordering;
|
|
|
|
pub(crate) struct VmRuntimeHost<'a> {
|
|
pub runtime: &'a mut VirtualMachineRuntime,
|
|
pub log_service: &'a mut LogService,
|
|
pub fs: &'a mut VirtualFS,
|
|
pub fs_state: &'a mut FsState,
|
|
pub memcard: &'a mut MemcardService,
|
|
pub open_files: &'a mut HashMap<u32, String>,
|
|
pub next_handle: &'a mut u32,
|
|
}
|
|
|
|
impl Deref for VmRuntimeHost<'_> {
|
|
type Target = VirtualMachineRuntime;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
self.runtime
|
|
}
|
|
}
|
|
|
|
impl DerefMut for VmRuntimeHost<'_> {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
self.runtime
|
|
}
|
|
}
|
|
|
|
impl VmRuntimeHost<'_> {
|
|
fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) {
|
|
self.runtime.log(self.log_service, level, source, tag, msg);
|
|
}
|
|
|
|
fn panic_payload_to_string(payload: Box<dyn std::any::Any + Send>) -> String {
|
|
if let Some(message) = payload.downcast_ref::<String>() {
|
|
return message.clone();
|
|
}
|
|
if let Some(message) = payload.downcast_ref::<&str>() {
|
|
return (*message).to_string();
|
|
}
|
|
"host panic without string payload".to_string()
|
|
}
|
|
|
|
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> {
|
|
let level = match level_val {
|
|
0 => LogLevel::Trace,
|
|
1 => LogLevel::Debug,
|
|
2 => LogLevel::Info,
|
|
3 => LogLevel::Warn,
|
|
4 => LogLevel::Error,
|
|
_ => return Err(VmFault::Trap(TRAP_TYPE, format!("Invalid log level: {}", level_val))),
|
|
};
|
|
|
|
let app_id = self.current_app_id;
|
|
let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0);
|
|
|
|
if count >= VirtualMachineRuntime::MAX_LOGS_PER_FRAME {
|
|
if count == VirtualMachineRuntime::MAX_LOGS_PER_FRAME {
|
|
self.logs_written_this_frame.insert(app_id, count + 1);
|
|
self.log(
|
|
LogLevel::Warn,
|
|
LogSource::App { app_id },
|
|
0,
|
|
"App exceeded log limit per frame".to_string(),
|
|
);
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
self.logs_written_this_frame.insert(app_id, count + 1);
|
|
|
|
let mut final_msg = msg;
|
|
if final_msg.len() > VirtualMachineRuntime::MAX_LOG_LEN {
|
|
final_msg.truncate(VirtualMachineRuntime::MAX_LOG_LEN);
|
|
}
|
|
|
|
self.log(level, LogSource::App { app_id }, tag, final_msg);
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn get_color(&self, value: i64) -> Result<Color, VmFault> {
|
|
let raw = u32::try_from(value)
|
|
.map_err(|_| VmFault::Trap(TRAP_OOB, "Color value out of bounds".into()))?;
|
|
Ok(Color::from_raw(raw))
|
|
}
|
|
|
|
fn int_arg_to_usize_status(value: i64) -> Result<usize, ComposerOpStatus> {
|
|
usize::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
|
|
}
|
|
|
|
fn int_arg_to_i32_trap(value: i64, name: &str) -> Result<i32, VmFault> {
|
|
i32::try_from(value)
|
|
.map_err(|_| VmFault::Trap(TRAP_OOB, format!("{name} value out of bounds")))
|
|
}
|
|
|
|
fn int_arg_to_u8_status(value: i64) -> Result<u8, ComposerOpStatus> {
|
|
u8::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
|
|
}
|
|
|
|
fn int_arg_to_u16_status(value: i64) -> Result<u16, ComposerOpStatus> {
|
|
u16::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
|
|
}
|
|
|
|
fn is_game_profile_syscall(syscall: Syscall) -> bool {
|
|
matches!(
|
|
syscall,
|
|
Syscall::GfxClear
|
|
| Syscall::GfxFillRect
|
|
| Syscall::GfxDrawLine
|
|
| Syscall::GfxDrawCircle
|
|
| Syscall::GfxDrawDisc
|
|
| Syscall::GfxDrawSquare
|
|
| Syscall::GfxDrawText
|
|
| Syscall::ComposerBindScene
|
|
| Syscall::ComposerUnbindScene
|
|
| Syscall::ComposerSetCamera
|
|
| Syscall::ComposerEmitSprite
|
|
| Syscall::AssetLoad
|
|
| Syscall::AssetStatus
|
|
| Syscall::AssetCommit
|
|
| Syscall::AssetCancel
|
|
| Syscall::BankInfo
|
|
)
|
|
}
|
|
|
|
fn ensure_game_profile_syscall(&self, syscall: Syscall) -> Result<(), VmFault> {
|
|
if Self::is_game_profile_syscall(syscall)
|
|
&& self.current_cartridge_app_mode != AppMode::Game
|
|
{
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Game profile", syscall.name()),
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl NativeInterface for VmRuntimeHost<'_> {
|
|
fn syscall(
|
|
&mut self,
|
|
id: SyscallId,
|
|
args: &[Value],
|
|
ret: &mut HostReturn,
|
|
ctx: &mut HostContext,
|
|
) -> Result<(), VmFault> {
|
|
self.atomic_telemetry.syscalls.fetch_add(1, Ordering::Relaxed);
|
|
let syscall = Syscall::from_u32(id).ok_or_else(|| {
|
|
VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id))
|
|
})?;
|
|
|
|
match syscall {
|
|
Syscall::SystemHasCart => {
|
|
ret.push_bool(true);
|
|
return Ok(());
|
|
}
|
|
Syscall::SystemRunCart => return Ok(()),
|
|
_ => {}
|
|
}
|
|
|
|
self.ensure_game_profile_syscall(syscall)?;
|
|
|
|
let platform = ctx.require_platform()?;
|
|
|
|
match syscall {
|
|
Syscall::SystemHasCart => unreachable!(),
|
|
Syscall::SystemRunCart => unreachable!(),
|
|
Syscall::GfxClear => {
|
|
let color = self.get_color(expect_int(args, 0)?)?;
|
|
self.gfx2d_commands.push(Gfx2dCommand::Clear { color });
|
|
Ok(())
|
|
}
|
|
Syscall::GfxUiClear => {
|
|
if self.current_cartridge_app_mode != AppMode::Shell {
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Shell profile", syscall.name()),
|
|
));
|
|
}
|
|
let color = self.get_color(expect_int(args, 0)?)?;
|
|
self.gfxui_commands.push(GfxUiCommand::Clear { color });
|
|
Ok(())
|
|
}
|
|
Syscall::GfxFillRect => {
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let w = expect_int(args, 2)? as i32;
|
|
let h = expect_int(args, 3)? as i32;
|
|
let color = self.get_color(expect_int(args, 4)?)?;
|
|
self.gfx2d_commands
|
|
.push(Gfx2dCommand::FillRect { rect: Rect { x, y, w, h }, color });
|
|
Ok(())
|
|
}
|
|
Syscall::GfxUiFillRect => {
|
|
if self.current_cartridge_app_mode != AppMode::Shell {
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Shell profile", syscall.name()),
|
|
));
|
|
}
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let w = expect_int(args, 2)? as i32;
|
|
let h = expect_int(args, 3)? as i32;
|
|
let color = self.get_color(expect_int(args, 4)?)?;
|
|
self.gfxui_commands
|
|
.push(GfxUiCommand::FillRect { rect: Rect { x, y, w, h }, color });
|
|
Ok(())
|
|
}
|
|
Syscall::GfxDrawLine => {
|
|
let x1 = expect_int(args, 0)? as i32;
|
|
let y1 = expect_int(args, 1)? as i32;
|
|
let x2 = expect_int(args, 2)? as i32;
|
|
let y2 = expect_int(args, 3)? as i32;
|
|
let color = self.get_color(expect_int(args, 4)?)?;
|
|
self.gfx2d_commands.push(Gfx2dCommand::DrawLine {
|
|
x0: x1,
|
|
y0: y1,
|
|
x1: x2,
|
|
y1: y2,
|
|
color,
|
|
});
|
|
Ok(())
|
|
}
|
|
Syscall::GfxUiDrawLine => {
|
|
if self.current_cartridge_app_mode != AppMode::Shell {
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Shell profile", syscall.name()),
|
|
));
|
|
}
|
|
let x1 = expect_int(args, 0)? as i32;
|
|
let y1 = expect_int(args, 1)? as i32;
|
|
let x2 = expect_int(args, 2)? as i32;
|
|
let y2 = expect_int(args, 3)? as i32;
|
|
let color = self.get_color(expect_int(args, 4)?)?;
|
|
self.gfxui_commands.push(GfxUiCommand::DrawLine {
|
|
x0: x1,
|
|
y0: y1,
|
|
x1: x2,
|
|
y1: y2,
|
|
color,
|
|
});
|
|
Ok(())
|
|
}
|
|
Syscall::GfxDrawCircle => {
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let r = expect_int(args, 2)? as i32;
|
|
let color = self.get_color(expect_int(args, 3)?)?;
|
|
self.gfx2d_commands.push(Gfx2dCommand::DrawCircle { x, y, radius: r, color });
|
|
Ok(())
|
|
}
|
|
Syscall::GfxUiDrawCircle => {
|
|
if self.current_cartridge_app_mode != AppMode::Shell {
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Shell profile", syscall.name()),
|
|
));
|
|
}
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let r = expect_int(args, 2)? as i32;
|
|
let color = self.get_color(expect_int(args, 3)?)?;
|
|
self.gfxui_commands.push(GfxUiCommand::DrawCircle { x, y, radius: r, color });
|
|
Ok(())
|
|
}
|
|
Syscall::GfxDrawDisc => {
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let r = expect_int(args, 2)? as i32;
|
|
let border_color = self.get_color(expect_int(args, 3)?)?;
|
|
let fill_color = self.get_color(expect_int(args, 4)?)?;
|
|
self.gfx2d_commands.push(Gfx2dCommand::DrawDisc {
|
|
x,
|
|
y,
|
|
radius: r,
|
|
border_color,
|
|
fill_color,
|
|
});
|
|
Ok(())
|
|
}
|
|
Syscall::GfxUiDrawDisc => {
|
|
if self.current_cartridge_app_mode != AppMode::Shell {
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Shell profile", syscall.name()),
|
|
));
|
|
}
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let r = expect_int(args, 2)? as i32;
|
|
let border_color = self.get_color(expect_int(args, 3)?)?;
|
|
let fill_color = self.get_color(expect_int(args, 4)?)?;
|
|
self.gfxui_commands.push(GfxUiCommand::DrawDisc {
|
|
x,
|
|
y,
|
|
radius: r,
|
|
border_color,
|
|
fill_color,
|
|
});
|
|
Ok(())
|
|
}
|
|
Syscall::GfxDrawSquare => {
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let w = expect_int(args, 2)? as i32;
|
|
let h = expect_int(args, 3)? as i32;
|
|
let border_color = self.get_color(expect_int(args, 4)?)?;
|
|
let fill_color = self.get_color(expect_int(args, 5)?)?;
|
|
self.gfx2d_commands.push(Gfx2dCommand::DrawSquare {
|
|
rect: Rect { x, y, w, h },
|
|
border_color,
|
|
fill_color,
|
|
});
|
|
Ok(())
|
|
}
|
|
Syscall::GfxUiDrawSquare => {
|
|
if self.current_cartridge_app_mode != AppMode::Shell {
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Shell profile", syscall.name()),
|
|
));
|
|
}
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let w = expect_int(args, 2)? as i32;
|
|
let h = expect_int(args, 3)? as i32;
|
|
let border_color = self.get_color(expect_int(args, 4)?)?;
|
|
let fill_color = self.get_color(expect_int(args, 5)?)?;
|
|
self.gfxui_commands.push(GfxUiCommand::DrawSquare {
|
|
rect: Rect { x, y, w, h },
|
|
border_color,
|
|
fill_color,
|
|
});
|
|
Ok(())
|
|
}
|
|
Syscall::GfxDrawText => {
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let msg = expect_string(args, 2, "message")?;
|
|
let color = self.get_color(expect_int(args, 3)?)?;
|
|
self.gfx2d_commands.push(Gfx2dCommand::DrawText { x, y, text: msg.clone(), color });
|
|
Ok(())
|
|
}
|
|
Syscall::GfxUiDrawText => {
|
|
if self.current_cartridge_app_mode != AppMode::Shell {
|
|
return Err(VmFault::Trap(
|
|
TRAP_INVALID_SYSCALL,
|
|
format!("{} is only available to the Shell profile", syscall.name()),
|
|
));
|
|
}
|
|
let x = expect_int(args, 0)? as i32;
|
|
let y = expect_int(args, 1)? as i32;
|
|
let msg = expect_string(args, 2, "message")?;
|
|
let color = self.get_color(expect_int(args, 3)?)?;
|
|
self.gfxui_commands.push(GfxUiCommand::DrawText { x, y, text: msg.clone(), color });
|
|
Ok(())
|
|
}
|
|
Syscall::ComposerBindScene => {
|
|
let scene_bank_id = match Self::int_arg_to_usize_status(expect_int(args, 0)?) {
|
|
Ok(id) => id,
|
|
Err(status) => {
|
|
ret.push_int(status as i64);
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let status = catch_unwind(AssertUnwindSafe(|| {
|
|
platform.game2d_frame_composer().bind_scene(scene_bank_id)
|
|
}))
|
|
.map_err(|payload| VmFault::Panic(Self::panic_payload_to_string(payload)))?;
|
|
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::ComposerUnbindScene => {
|
|
platform.game2d_frame_composer().unbind_scene();
|
|
ret.push_int(ComposerOpStatus::Ok as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::ComposerSetCamera => {
|
|
let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?;
|
|
let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?;
|
|
platform.game2d_frame_composer().set_camera(x, y);
|
|
Ok(())
|
|
}
|
|
Syscall::ComposerEmitSprite => {
|
|
let glyph_id = match Self::int_arg_to_u16_status(expect_int(args, 0)?) {
|
|
Ok(value) => value,
|
|
Err(status) => {
|
|
ret.push_int(status as i64);
|
|
return Ok(());
|
|
}
|
|
};
|
|
let palette_id = match Self::int_arg_to_u8_status(expect_int(args, 1)?) {
|
|
Ok(value) if value < 64 => value,
|
|
_ => {
|
|
ret.push_int(ComposerOpStatus::ArgRangeInvalid as i64);
|
|
return Ok(());
|
|
}
|
|
};
|
|
let x = Self::int_arg_to_i32_trap(expect_int(args, 2)?, "sprite x")?;
|
|
let y = Self::int_arg_to_i32_trap(expect_int(args, 3)?, "sprite y")?;
|
|
let layer = match Self::int_arg_to_u8_status(expect_int(args, 4)?) {
|
|
Ok(value) if value < 4 => value,
|
|
Ok(_) => {
|
|
ret.push_int(ComposerOpStatus::LayerInvalid as i64);
|
|
return Ok(());
|
|
}
|
|
Err(status) => {
|
|
ret.push_int(status as i64);
|
|
return Ok(());
|
|
}
|
|
};
|
|
let bank_id = match Self::int_arg_to_u8_status(expect_int(args, 5)?) {
|
|
Ok(value) => value,
|
|
Err(status) => {
|
|
ret.push_int(status as i64);
|
|
return Ok(());
|
|
}
|
|
};
|
|
let flip_x = expect_bool(args, 6)?;
|
|
let flip_y = expect_bool(args, 7)?;
|
|
let priority = match Self::int_arg_to_u8_status(expect_int(args, 8)?) {
|
|
Ok(value) => value,
|
|
Err(status) => {
|
|
ret.push_int(status as i64);
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let status = platform.game2d_frame_composer().emit_sprite(Sprite {
|
|
glyph: Glyph { glyph_id, palette_id },
|
|
x,
|
|
y,
|
|
layer,
|
|
bank_id,
|
|
active: false,
|
|
flip_x,
|
|
flip_y,
|
|
priority,
|
|
});
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AudioPlaySample => {
|
|
let sample_id_raw = expect_int(args, 0)?;
|
|
let voice_id_raw = expect_int(args, 1)?;
|
|
let volume_raw = expect_int(args, 2)?;
|
|
let pan_raw = expect_int(args, 3)?;
|
|
let pitch = expect_number(args, 4, "pitch")?;
|
|
|
|
if !(0..16).contains(&voice_id_raw) {
|
|
ret.push_int(AudioOpStatus::VoiceInvalid as i64);
|
|
return Ok(());
|
|
}
|
|
|
|
if sample_id_raw < 0
|
|
|| sample_id_raw > u16::MAX as i64
|
|
|| !(0..=255).contains(&volume_raw)
|
|
|| !(0..=255).contains(&pan_raw)
|
|
|| !pitch.is_finite()
|
|
|| pitch <= 0.0
|
|
{
|
|
ret.push_int(AudioOpStatus::ArgRangeInvalid as i64);
|
|
return Ok(());
|
|
}
|
|
|
|
let status = platform.audio_mut().play(
|
|
0,
|
|
sample_id_raw as u16,
|
|
voice_id_raw as usize,
|
|
volume_raw as u8,
|
|
pan_raw as u8,
|
|
pitch,
|
|
0,
|
|
prometeu_hal::LoopMode::Off,
|
|
);
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AudioPlay => {
|
|
let bank_id = expect_int(args, 0)? as u8;
|
|
let sample_id_raw = expect_int(args, 1)?;
|
|
let voice_id_raw = expect_int(args, 2)?;
|
|
let volume_raw = expect_int(args, 3)?;
|
|
let pan_raw = expect_int(args, 4)?;
|
|
let pitch = expect_number(args, 5, "pitch")?;
|
|
let loop_mode = match expect_int(args, 6)? {
|
|
0 => prometeu_hal::LoopMode::Off,
|
|
_ => prometeu_hal::LoopMode::On,
|
|
};
|
|
|
|
if !(0..16).contains(&voice_id_raw) {
|
|
ret.push_int(AudioOpStatus::VoiceInvalid as i64);
|
|
return Ok(());
|
|
}
|
|
|
|
if platform.assets().slot_info(SlotRef::audio(bank_id as usize)).asset_id.is_none()
|
|
{
|
|
ret.push_int(AudioOpStatus::BankInvalid as i64);
|
|
return Ok(());
|
|
}
|
|
|
|
if sample_id_raw < 0
|
|
|| sample_id_raw > u16::MAX as i64
|
|
|| !(0..=255).contains(&volume_raw)
|
|
|| !(0..=255).contains(&pan_raw)
|
|
|| !pitch.is_finite()
|
|
|| pitch <= 0.0
|
|
{
|
|
ret.push_int(AudioOpStatus::ArgRangeInvalid as i64);
|
|
return Ok(());
|
|
}
|
|
|
|
let status = platform.audio_mut().play(
|
|
bank_id,
|
|
sample_id_raw as u16,
|
|
voice_id_raw as usize,
|
|
volume_raw as u8,
|
|
pan_raw as u8,
|
|
pitch,
|
|
0,
|
|
loop_mode,
|
|
);
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::FsOpen => {
|
|
let path = expect_string(args, 0, "path")?;
|
|
if *self.fs_state != FsState::Mounted {
|
|
ret.push_int(-1);
|
|
return Ok(());
|
|
}
|
|
let handle = *self.next_handle;
|
|
self.open_files.insert(handle, path);
|
|
*self.next_handle += 1;
|
|
ret.push_int(handle as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::FsRead => {
|
|
let handle = expect_int(args, 0)? as u32;
|
|
let path = self
|
|
.open_files
|
|
.get(&handle)
|
|
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?
|
|
.clone();
|
|
match self.fs.read_file(&path) {
|
|
Ok(data) => ret.push_string(String::from_utf8_lossy(&data).into_owned()),
|
|
Err(_) => ret.push_null(),
|
|
}
|
|
Ok(())
|
|
}
|
|
Syscall::FsWrite => {
|
|
let handle = expect_int(args, 0)? as u32;
|
|
let content = expect_string(args, 1, "content")?.into_bytes();
|
|
let path = self
|
|
.open_files
|
|
.get(&handle)
|
|
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?
|
|
.clone();
|
|
match self.fs.write_file(&path, &content) {
|
|
Ok(_) => ret.push_bool(true),
|
|
Err(_) => ret.push_bool(false),
|
|
}
|
|
Ok(())
|
|
}
|
|
Syscall::FsClose => {
|
|
self.open_files.remove(&(expect_int(args, 0)? as u32));
|
|
Ok(())
|
|
}
|
|
Syscall::FsListDir => {
|
|
let path = expect_string(args, 0, "path")?;
|
|
match self.fs.list_dir(&path) {
|
|
Ok(entries) => {
|
|
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
|
|
ret.push_string(names.join(";"));
|
|
}
|
|
Err(_) => ret.push_null(),
|
|
}
|
|
Ok(())
|
|
}
|
|
Syscall::FsExists => {
|
|
ret.push_bool(self.fs.exists(&expect_string(args, 0, "path")?));
|
|
Ok(())
|
|
}
|
|
Syscall::FsDelete => {
|
|
match self.fs.delete(&expect_string(args, 0, "path")?) {
|
|
Ok(_) => ret.push_bool(true),
|
|
Err(_) => ret.push_bool(false),
|
|
}
|
|
Ok(())
|
|
}
|
|
Syscall::MemSlotCount => {
|
|
if self.current_cartridge_app_mode != AppMode::Game {
|
|
ret.push_int(MemcardStatus::AccessDenied as i64);
|
|
ret.push_int(0);
|
|
return Ok(());
|
|
}
|
|
ret.push_int(MemcardStatus::Ok as i64);
|
|
ret.push_int(self.memcard.slot_count() as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::MemSlotStat => {
|
|
let slot = expect_slot_index(args, 0)?;
|
|
if self.current_cartridge_app_mode != AppMode::Game {
|
|
ret.push_int(MemcardStatus::AccessDenied as i64);
|
|
ret.push_int(MemcardSlotState::Empty as i64);
|
|
ret.push_int(0);
|
|
ret.push_int(0);
|
|
ret.push_int(0);
|
|
return Ok(());
|
|
}
|
|
let stat = self.memcard.slot_stat(self.fs, self.current_app_id, slot);
|
|
let status = if stat.state == MemcardSlotState::Corrupt {
|
|
MemcardStatus::Corrupt
|
|
} else {
|
|
MemcardStatus::Ok
|
|
};
|
|
ret.push_int(status as i64);
|
|
ret.push_int(stat.state as i64);
|
|
ret.push_int(stat.used_bytes as i64);
|
|
ret.push_int(stat.generation as i64);
|
|
ret.push_int(stat.checksum as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::MemSlotRead => {
|
|
let slot = expect_slot_index(args, 0)?;
|
|
let offset = expect_non_negative_usize(args, 1, "offset")?;
|
|
let max_bytes = expect_non_negative_usize(args, 2, "max_bytes")?;
|
|
if self.current_cartridge_app_mode != AppMode::Game {
|
|
ret.push_int(MemcardStatus::AccessDenied as i64);
|
|
ret.push_string(String::new());
|
|
ret.push_int(0);
|
|
return Ok(());
|
|
}
|
|
let read =
|
|
self.memcard.slot_read(self.fs, self.current_app_id, slot, offset, max_bytes);
|
|
ret.push_int(read.status as i64);
|
|
ret.push_string(hex_encode(&read.bytes));
|
|
ret.push_int(read.bytes_read as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::MemSlotWrite => {
|
|
let slot = expect_slot_index(args, 0)?;
|
|
let offset = expect_non_negative_usize(args, 1, "offset")?;
|
|
let payload_hex = expect_string(args, 2, "payload_hex")?;
|
|
if self.current_cartridge_app_mode != AppMode::Game {
|
|
ret.push_int(MemcardStatus::AccessDenied as i64);
|
|
ret.push_int(0);
|
|
return Ok(());
|
|
}
|
|
let payload = hex_decode(&payload_hex)?;
|
|
let app_id = self.current_app_id;
|
|
let write = self.memcard.slot_write(self.fs, app_id, slot, offset, &payload);
|
|
ret.push_int(write.status as i64);
|
|
ret.push_int(write.bytes_written as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::MemSlotCommit => {
|
|
let slot = expect_slot_index(args, 0)?;
|
|
if self.current_cartridge_app_mode != AppMode::Game {
|
|
ret.push_int(MemcardStatus::AccessDenied as i64);
|
|
return Ok(());
|
|
}
|
|
let status = {
|
|
let app_id = self.current_app_id;
|
|
let memcard = &mut self.memcard;
|
|
let fs = &mut self.fs;
|
|
memcard.slot_commit(fs, app_id, slot)
|
|
};
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::MemSlotClear => {
|
|
let slot = expect_slot_index(args, 0)?;
|
|
if self.current_cartridge_app_mode != AppMode::Game {
|
|
ret.push_int(MemcardStatus::AccessDenied as i64);
|
|
return Ok(());
|
|
}
|
|
let status = {
|
|
let app_id = self.current_app_id;
|
|
let memcard = &mut self.memcard;
|
|
let fs = &mut self.fs;
|
|
memcard.slot_clear(fs, app_id, slot)
|
|
};
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::LogWrite => {
|
|
self.syscall_log_write(
|
|
expect_int(args, 0)?,
|
|
0,
|
|
expect_string(args, 1, "message")?,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
Syscall::LogWriteTag => {
|
|
self.syscall_log_write(
|
|
expect_int(args, 0)?,
|
|
expect_int(args, 1)? as u16,
|
|
expect_string(args, 2, "message")?,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
Syscall::AssetLoad => {
|
|
let raw_asset_id = expect_int(args, 0)?;
|
|
let asset_id = AssetId::try_from(raw_asset_id).map_err(|_| {
|
|
VmFault::Trap(TRAP_TYPE, format!("asset_id out of i32 range: {}", raw_asset_id))
|
|
})?;
|
|
let slot_index = expect_int(args, 1)? as usize;
|
|
|
|
match platform.assets().load(asset_id, slot_index) {
|
|
Ok(handle) => {
|
|
ret.push_int(AssetOpStatus::Ok as i64);
|
|
ret.push_int(handle as i64);
|
|
Ok(())
|
|
}
|
|
Err(status) => {
|
|
ret.push_int(status as i64);
|
|
ret.push_int(0);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
Syscall::AssetStatus => {
|
|
ret.push_int(platform.assets().status(expect_int(args, 0)? as u32) as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetCommit => {
|
|
let status = platform.assets().commit(expect_int(args, 0)? as u32);
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetCancel => {
|
|
let status = platform.assets().cancel(expect_int(args, 0)? as u32);
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetBacklogInfo => {
|
|
let info = platform.assets().backlog_info();
|
|
ret.push_int(info.status as i64);
|
|
ret.push_int(info.pending_count as i64);
|
|
ret.push_int(info.active_handle as i64);
|
|
ret.push_int(info.active_asset_id.unwrap_or(0) as i64);
|
|
ret.push_int(info.active_bank_type.map(|bank| bank as i64).unwrap_or(0));
|
|
ret.push_int(info.active_slot.map(|slot| slot as i64).unwrap_or(0));
|
|
ret.push_int(info.active_progress as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetBacklogPosition => {
|
|
let position = platform.assets().backlog_position(expect_int(args, 0)? as u32);
|
|
ret.push_int(position.status as i64);
|
|
ret.push_int(position.state as i64);
|
|
ret.push_int(position.position as i64);
|
|
ret.push_int(position.progress as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetBacklogMove => {
|
|
let new_position = expect_non_negative_usize(args, 1, "new_position")?;
|
|
let status =
|
|
platform.assets().backlog_move(expect_int(args, 0)? as u32, new_position);
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetBacklogPromote => {
|
|
let status = platform.assets().backlog_promote(expect_int(args, 0)? as u32);
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetBacklogDemote => {
|
|
let status = platform.assets().backlog_demote(expect_int(args, 0)? as u32);
|
|
ret.push_int(status as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::AssetTargetStatus => {
|
|
let bank_type = match expect_int(args, 0)? {
|
|
0 => BankType::GLYPH,
|
|
1 => BankType::SOUNDS,
|
|
2 => BankType::SCENE,
|
|
other => {
|
|
return Err(VmFault::Trap(
|
|
TRAP_TYPE,
|
|
format!("Invalid asset bank type: {}", other),
|
|
));
|
|
}
|
|
};
|
|
let slot = expect_non_negative_usize(args, 1, "slot")?;
|
|
let target = platform.assets().target_status(bank_type, slot);
|
|
ret.push_int(target.status as i64);
|
|
ret.push_int(target.asset_id.unwrap_or(0) as i64);
|
|
ret.push_int(target.handle as i64);
|
|
ret.push_int(target.state as i64);
|
|
ret.push_int(target.position as i64);
|
|
ret.push_int(target.progress as i64);
|
|
Ok(())
|
|
}
|
|
Syscall::BankInfo => {
|
|
let asset_type = match expect_int(args, 0)? as u32 {
|
|
0 => BankType::GLYPH,
|
|
1 => BankType::SOUNDS,
|
|
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
|
|
};
|
|
let telemetry = platform
|
|
.assets()
|
|
.bank_telemetry()
|
|
.into_iter()
|
|
.find(|entry| entry.bank_type == asset_type)
|
|
.unwrap_or(prometeu_hal::asset::BankTelemetry {
|
|
bank_type: asset_type,
|
|
used_slots: 0,
|
|
total_slots: 0,
|
|
});
|
|
ret.push_int(telemetry.used_slots as i64);
|
|
ret.push_int(telemetry.total_slots as i64);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn expect_string(args: &[Value], index: usize, field: &str) -> Result<String, VmFault> {
|
|
match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? {
|
|
Value::String(value) => Ok(value.to_string()),
|
|
_ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected string {}", field))),
|
|
}
|
|
}
|
|
|
|
fn expect_number(args: &[Value], index: usize, field: &str) -> Result<f64, VmFault> {
|
|
match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? {
|
|
Value::Float(f) => Ok(*f),
|
|
Value::Int32(i) => Ok(*i as f64),
|
|
Value::Int64(i) => Ok(*i as f64),
|
|
_ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected number for {}", field))),
|
|
}
|
|
}
|
|
|
|
fn expect_slot_index(args: &[Value], index: usize) -> Result<u8, VmFault> {
|
|
let slot = expect_int(args, index)?;
|
|
if !(0..32).contains(&slot) {
|
|
return Err(VmFault::Trap(TRAP_OOB, format!("slot index out of bounds: {}", slot)));
|
|
}
|
|
Ok(slot as u8)
|
|
}
|
|
|
|
fn expect_non_negative_usize(args: &[Value], index: usize, field: &str) -> Result<usize, VmFault> {
|
|
let val = expect_int(args, index)?;
|
|
if val < 0 {
|
|
return Err(VmFault::Trap(TRAP_OOB, format!("{} must be non-negative", field)));
|
|
}
|
|
Ok(val as usize)
|
|
}
|
|
|
|
fn hex_encode(bytes: &[u8]) -> String {
|
|
const HEX: &[u8; 16] = b"0123456789abcdef";
|
|
let mut out = String::with_capacity(bytes.len() * 2);
|
|
for &b in bytes {
|
|
out.push(HEX[(b >> 4) as usize] as char);
|
|
out.push(HEX[(b & 0x0f) as usize] as char);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn hex_decode(s: &str) -> Result<Vec<u8>, VmFault> {
|
|
fn nibble(c: u8) -> Option<u8> {
|
|
match c {
|
|
b'0'..=b'9' => Some(c - b'0'),
|
|
b'a'..=b'f' => Some(10 + c - b'a'),
|
|
b'A'..=b'F' => Some(10 + c - b'A'),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
let bytes = s.as_bytes();
|
|
if !bytes.len().is_multiple_of(2) {
|
|
return Err(VmFault::Trap(TRAP_TYPE, "payload_hex must have even length".to_string()));
|
|
}
|
|
let mut out = Vec::with_capacity(bytes.len() / 2);
|
|
let mut i = 0usize;
|
|
while i < bytes.len() {
|
|
let hi = nibble(bytes[i]).ok_or_else(|| {
|
|
VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string())
|
|
})?;
|
|
let lo = nibble(bytes[i + 1]).ok_or_else(|| {
|
|
VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string())
|
|
})?;
|
|
out.push((hi << 4) | lo);
|
|
i += 2;
|
|
}
|
|
Ok(out)
|
|
}
|