This commit is contained in:
2026-04-28 19:36:39 +08:00
parent a9febe7678
commit f0471a4f8d
206 changed files with 18456 additions and 10133 deletions

View File

@@ -2,7 +2,10 @@
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import { isAbortError } from '../../services/apiClient';
import { rpgSnapshotClient } from '../../services/rpg-runtime';
import {
getRpgRuntimeSessionId,
rpgSnapshotClient,
} from '../../services/rpg-runtime';
import type { GameState, StoryMoment } from '../../types';
import { resumeServerRuntimeStory } from '../rpg-runtime-story/runtimeStoryCoordinator';
import type { BottomTab } from './rpgSessionTypes';
@@ -10,6 +13,8 @@ import type { BottomTab } from './rpgSessionTypes';
const AUTO_SAVE_DELAY_MS = 400;
function canPersistSnapshot(gameState: GameState, story: StoryMoment | null) {
// 中文注释preview / test 模式、非 Story 场景、未选角、以及流式输出中的故事都不应入正式存档,
// 否则容易把临时态或半成品叙事写进继续游戏链路。
return (
gameState.runtimePersistenceDisabled !== true &&
gameState.runtimeMode !== 'preview' &&
@@ -30,6 +35,8 @@ function normalizeBottomTab(bottomTab: string | null | undefined): BottomTab {
}
function resolveRemoteSnapshotState(snapshot: HydratedSavedGameSnapshot) {
// 中文注释:远端快照允许缺少局部 UI 状态;
// 这里统一补底部 tab 的兜底值,避免恢复后落到非法面板名。
return {
gameState: snapshot.gameState,
currentStory: snapshot.currentStory ?? null,
@@ -75,6 +82,8 @@ export function useRpgSessionPersistence({
const saveRequestIdRef = useRef(0);
const abortActiveSave = useCallback(() => {
// 中文注释:自动存档是“后写覆盖前写”的串行语义;
// 新一次保存开始前,主动打断旧请求,避免旧快照回写覆盖最新状态。
saveControllerRef.current?.abort();
saveControllerRef.current = null;
setIsPersistingSnapshot(false);
@@ -83,9 +92,8 @@ export function useRpgSessionPersistence({
const persistSnapshot = useCallback(
async (params: {
payload: {
gameState: GameState;
sessionId: string;
bottomTab: BottomTab;
currentStory: StoryMoment | null;
};
logLabel: string;
}) => {
@@ -103,11 +111,12 @@ export function useRpgSessionPersistence({
setPersistenceError(null);
try {
// 中文注释:这里不再上传整份本地快照;
// 前端只告诉后端“当前 session 需要 checkpoint”真实 GameState 由服务端快照表读取。
const snapshot = await rpgSnapshotClient.putSnapshot(
{
gameState: params.payload.gameState,
sessionId: params.payload.sessionId,
bottomTab: params.payload.bottomTab,
currentStory: params.payload.currentStory,
},
{ signal: controller.signal },
);
@@ -158,6 +167,8 @@ export function useRpgSessionPersistence({
hydrateControllerRef.current = controller;
setIsHydratingSnapshot(true);
// 中文注释:登录后第一时间探测一次远端快照,
// 让入口页能够准确判断“继续游戏”按钮是否可见。
void rpgSnapshotClient
.getSnapshot({ signal: controller.signal })
.then((snapshot) => {
@@ -207,12 +218,13 @@ export function useRpgSessionPersistence({
if (!canPersist) return;
// 中文注释:自动存档做一个很短的去抖,
// 避免同一轮状态连锁更新时重复打多次快照请求。
const timeoutId = window.setTimeout(() => {
void persistSnapshot({
payload: {
gameState,
sessionId: getRpgRuntimeSessionId(gameState),
bottomTab,
currentStory,
},
logLabel: 'failed to autosave remote snapshot',
});
@@ -235,11 +247,12 @@ export function useRpgSessionPersistence({
return false;
}
// 中文注释:手动存档和自动存档走同一套底层 persist 逻辑,
// 差别只在于调用方可显式覆盖本次 checkpoint 的 session 与 UI tab。
const snapshot = await persistSnapshot({
payload: {
gameState: nextGameState,
sessionId: getRpgRuntimeSessionId(nextGameState),
bottomTab: nextBottomTab,
currentStory: nextStory,
},
logLabel: 'failed to save remote snapshot',
});
@@ -300,6 +313,8 @@ export function useRpgSessionPersistence({
resetStoryState();
const fallbackHydration = resolveRemoteSnapshotState(snapshot);
// 中文注释:继续游戏不是简单把旧 currentStory 塞回去,
// 还要向服务端刷新一遍 runtime story拿到当前服务端判定的可选动作与视图模型。
const resumedState = await resumeServerRuntimeStory(snapshot).catch(
(error) => {
if (!isAbortError(error)) {