implements PLN-0157

This commit is contained in:
bQUARKz 2026-07-05 01:07:54 +01:00
parent 0ffb3467b1
commit dcd3479b1d
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 103 additions and 37 deletions

View File

@ -52,45 +52,53 @@ impl HubHomeStep {
ctx.os.windows().set_focus(id); ctx.os.windows().set_focus(id);
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id))); return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
} }
Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) { Some(SystemProfileAction::LaunchGame { target }) => {
Ok(cartridge) => { match CartridgeLoader::load(&target.path) {
if cartridge.app_mode == AppMode::Game Ok(cartridge) => {
&& let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task() if cartridge.app_mode == AppMode::Game
{ && let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task()
let resident_app_id = ctx {
.os let resident_app_id = ctx
.sessions() .os
.vm_session_for_task(resident_task_id) .sessions()
.map(|session| session.app_id) .vm_session_for_task(resident_task_id)
.unwrap_or(0); .map(|session| session.app_id)
.unwrap_or(0);
if resident_app_id == cartridge.app_id { if resident_app_id == cartridge.app_id {
return Some(FirmwareState::ResumeResidentGame); return Some(FirmwareState::ResumeResidentGame);
}
ctx.os.log(
LogLevel::Warn,
LogSource::Hub,
0,
format!(
"Cannot launch Home game {} while resident game app_id {} is active",
cartridge.title, resident_app_id
),
);
return None;
} }
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(
cartridge,
)));
}
Err(error) => {
ctx.os.log( ctx.os.log(
LogLevel::Warn, LogLevel::Error,
LogSource::Hub, LogSource::Hub,
0, 0,
format!( format!(
"Cannot launch Home game {} while resident game app_id {} is active", "Failed to launch Home game {}: {:?}",
cartridge.title, resident_app_id target.path.display(),
error
), ),
); );
return None;
} }
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)));
} }
Err(error) => { }
ctx.os.log(
LogLevel::Error,
LogSource::Hub,
0,
format!("Failed to launch Home game {}: {:?}", path.display(), error),
);
}
},
Some(SystemProfileAction::CloseShell) | None => {} Some(SystemProfileAction::CloseShell) | None => {}
} }

View File

@ -20,7 +20,7 @@ pub use services::foreground::{
}; };
pub use services::fs; pub use services::fs;
pub use services::game_library::{ pub use services::game_library::{
GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, discover_games_root, GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, GameSwitchTarget, discover_games_root,
}; };
pub use services::memcard::MemcardAsyncLaneOperation; pub use services::memcard::MemcardAsyncLaneOperation;
pub use services::process; pub use services::process;

View File

@ -1,12 +1,11 @@
use crate::task::TaskId; use crate::task::TaskId;
use crate::{CrashReport, GameLibrary, SystemOS}; use crate::{CrashReport, GameLibrary, GameSwitchTarget, SystemOS};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect; use prometeu_hal::primitives::Rect;
use prometeu_hal::{ use prometeu_hal::{
FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform, FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform,
ShellUiFramePacket, ShellUiFramePacket,
}; };
use std::path::PathBuf;
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 }; const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 };
const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 172, w: 112, h: 32 }; const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 172, w: 112, h: 32 };
@ -83,7 +82,7 @@ impl NativeShellApp {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum SystemProfileAction { pub enum SystemProfileAction {
LaunchNativeShell(NativeShellApp), LaunchNativeShell(NativeShellApp),
LaunchGame { path: PathBuf }, LaunchGame { target: GameSwitchTarget },
CloseShell, CloseShell,
} }
@ -249,7 +248,7 @@ fn action_for_click(
.find(|(index, _entry)| rect_contains(game_row_rect(*index), x, y)) .find(|(index, _entry)| rect_contains(game_row_rect(*index), x, y))
.map(|(_index, entry)| entry) .map(|(_index, entry)| entry)
{ {
return Some(SystemProfileAction::LaunchGame { path: entry.path.clone() }); return Some(SystemProfileAction::LaunchGame { target: entry.switch_target() });
} }
HOME_BUTTONS HOME_BUTTONS
@ -434,6 +433,7 @@ mod tests {
use crate::windows::WindowOwner; use crate::windows::WindowOwner;
use prometeu_drivers::TestPlatform; use prometeu_drivers::TestPlatform;
use prometeu_hal::{InputSignals, RuntimePlatform}; use prometeu_hal::{InputSignals, RuntimePlatform};
use std::path::PathBuf;
fn focused_native_shell_fixture( fn focused_native_shell_fixture(
signals: &InputSignals, signals: &InputSignals,
@ -535,9 +535,19 @@ mod tests {
discovered_at: std::time::SystemTime::UNIX_EPOCH, discovered_at: std::time::SystemTime::UNIX_EPOCH,
}; };
let action =
action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry));
assert_eq!( assert_eq!(
action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)), action,
Some(SystemProfileAction::LaunchGame { path: entry.path.clone() }) Some(SystemProfileAction::LaunchGame {
target: GameSwitchTarget {
path: entry.path.clone(),
app_id: 42,
title: "Stress".to_string(),
app_version: "1.0.0".to_string(),
}
})
); );
} }

View File

@ -25,6 +25,29 @@ impl GameLibraryEntry {
discovered_at, discovered_at,
} }
} }
pub fn switch_target(&self) -> GameSwitchTarget {
GameSwitchTarget::from_entry(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GameSwitchTarget {
pub path: PathBuf,
pub app_id: u32,
pub title: String,
pub app_version: String,
}
impl GameSwitchTarget {
pub fn from_entry(entry: &GameLibraryEntry) -> Self {
Self {
path: entry.path.clone(),
app_id: entry.app_id,
title: entry.title.clone(),
app_version: entry.app_version.clone(),
}
}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -256,6 +279,31 @@ mod tests {
})); }));
} }
#[test]
fn switch_target_preserves_resolved_identity() {
let path = PathBuf::from("test-cartridges/stress-console");
let entry = GameLibraryEntry::new(
path.clone(),
CartridgeManifest {
magic: "PMTU".to_string(),
cartridge_version: 1,
app_id: 42,
title: "Stress".to_string(),
app_version: "1.0.0".to_string(),
app_mode: AppMode::Game,
capabilities: vec![],
},
UNIX_EPOCH,
);
let target = entry.switch_target();
assert_eq!(target.path, path);
assert_eq!(target.app_id, 42);
assert_eq!(target.title, "Stress");
assert_eq!(target.app_version, "1.0.0");
}
#[test] #[test]
fn missing_root_returns_diagnostic_and_no_entries() { fn missing_root_returns_diagnostic_and_no_entries() {
let root = TestRoot::new(); let root = TestRoot::new();

View File

@ -1,5 +1,5 @@
{"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}} {"type":"meta","next_id":{"DSC":44,"AGD":47,"DEC":40,"PLN":163,"LSN":53,"CLSN":1}}
{"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]} {"type":"discussion","id":"DSC-0043","status":"in_progress","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[{"id":"AGD-0044","file":"AGD-0044-systemos-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-05"}],"decisions":[{"id":"DEC-0039","file":"DEC-0039-systemos-game-cartridge-switch-orchestrator.md","status":"accepted","created_at":"2026-07-05","updated_at":"2026-07-05","ref_agenda":"AGD-0044"}],"plans":[{"id":"PLN-0157","file":"PLN-0157-introduce-systemos-game-switch-target-contract.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0158","file":"PLN-0158-terminate-resident-game-before-switching-cartridges.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0159","file":"PLN-0159-recover-failed-game-switches-back-to-home.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0160","file":"PLN-0160-add-dummy-boy-visual-switch-test-cartridge.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0161","file":"PLN-0161-validate-stress-to-dummy-boy-game-switch-end-to-end.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]},{"id":"PLN-0162","file":"PLN-0162-specify-game-switch-contract-and-direct-boot-separation.md","status":"open","created_at":"2026-07-05","updated_at":"2026-07-05","ref_decisions":["DEC-0039"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."} {"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
{"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]} {"type":"discussion","id":"DSC-0040","status":"done","ticket":"vm-render-parallel-execution-boundary","title":"VM and Render Parallel Execution Boundary","created_at":"2026-06-04","updated_at":"2026-06-06","tags":["runtime","renderer","vm","concurrency","architecture","perf"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0040-vm-render-parallel-execution-boundary/LSN-0048-render-workers-need-a-closed-packet-contract.md","status":"done","created_at":"2026-06-06","updated_at":"2026-06-06"}]}
{"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]} {"type":"discussion","id":"DSC-0041","status":"done","ticket":"foreground-stack-game-pause-shell-vm-backed","title":"Foreground Stack, Game Pause, and VM-Backed Shell Coexistence","created_at":"2026-06-05","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","shell","game","vm","foreground","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0052","file":"discussion/lessons/DSC-0041-foreground-stack-game-pause-shell-vm-backed/LSN-0052-foreground-ownership-and-vm-session-ownership-are-separate.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]}

View File

@ -2,7 +2,7 @@
id: PLN-0157 id: PLN-0157
ticket: system-os-cartridge-switch-orchestrator ticket: system-os-cartridge-switch-orchestrator
title: Introduce SystemOS Game Switch Target Contract title: Introduce SystemOS Game Switch Target Contract
status: open status: done
created: 2026-07-05 created: 2026-07-05
ref_decisions: [DEC-0039] ref_decisions: [DEC-0039]
tags: [runtime, os, lifecycle, game, cartridge, architecture] tags: [runtime, os, lifecycle, game, cartridge, architecture]