feat(runtime): prepare safe context checkpoints

This commit is contained in:
2026-07-29 08:46:23 +08:00
parent d0612b399c
commit b6600c1f06
64 changed files with 2552 additions and 187 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+53 -16
View File
@@ -515,6 +515,7 @@ impl DemoAppState {
.get("nana.ending.returned_before_dawn")
.copied()
.unwrap_or(false)
&& request.intent != TurnIntent::Regenerate
{
return Err(CommandError::invalid_input(
"这一夜的故事已经结束;可从回溯中选择另一条线路。",
@@ -965,7 +966,16 @@ impl TurnPlanProvider for DemoPlanProvider {
)
});
let (beats, delta, suggestions, can_continue) = if accepted_promise {
let (beats, delta, suggestions, can_continue) = if request.intent == TurnIntent::Regenerate
{
let (beats, delta) = regular_turn(request);
let ending = state
.world_flags
.get("nana.ending.returned_before_dawn")
.copied()
.unwrap_or(false);
(beats, delta, Vec::new(), !ending)
} else if accepted_promise {
let (beats, delta) = promise_turn(request, &committed_node_id);
(beats, delta, investigation_suggestions(), true)
} else if state
@@ -2188,6 +2198,47 @@ mod tests {
}
}
fn assert_ending_can_be_regenerated(app: &DemoAppState, ending_node_id: &str) {
let rejected = app
.submit_turn(&continue_request(
DEMO_BRANCH_ID,
ending_node_id,
"action_after_ending",
))
.expect_err("ending is terminal");
assert_eq!(rejected.code, "invalid_input");
assert_eq!(
app.store
.load_state(DEMO_STORY_ID, DEMO_BRANCH_ID)
.expect("unchanged ending")
.current_node,
ending_node_id
);
let mut regenerate =
continue_request(DEMO_BRANCH_ID, ending_node_id, "action_regenerate_ending");
regenerate.intent = TurnIntent::Regenerate;
regenerate.input = "this request text must not replace the original action".to_owned();
let regenerated = app
.submit_turn(&regenerate)
.expect("ending presentation can be regenerated");
assert_ne!(regenerated.player_view.branch_id, DEMO_BRANCH_ID);
assert!(!regenerated.player_view.can_continue);
assert_eq!(
app.store
.load_state(DEMO_STORY_ID, DEMO_BRANCH_ID)
.expect("original ending branch retained")
.current_node,
ending_node_id
);
let regenerated_node = app
.store
.load_node(DEMO_STORY_ID, &regenerated.committed_node_id)
.expect("regenerated ending node");
assert_eq!(regenerated_node.action_id, "action_return_before_dawn");
assert!(regenerated_node.user_input.is_empty());
}
fn node(id: &str, parent_id: Option<&str>, ops: Vec<StateOp>) -> StoryNode {
StoryNode {
id: id.to_owned(),
@@ -2312,21 +2363,7 @@ mod tests {
.get("nana.ending.returned_before_dawn"),
Some(&true)
);
let rejected = app
.submit_turn(&continue_request(
DEMO_BRANCH_ID,
&returned.committed_node_id,
"action_after_ending",
))
.expect_err("ending is terminal");
assert_eq!(rejected.code, "invalid_input");
assert_eq!(
app.store
.load_state(DEMO_STORY_ID, DEMO_BRANCH_ID)
.expect("unchanged ending")
.current_node,
returned.committed_node_id
);
assert_ending_can_be_regenerated(&app, &returned.committed_node_id);
}
#[test]