Fix stale restore after game restart
This commit is contained in:
@@ -30,6 +30,9 @@ class GameLoopModule extends BaseModule {
|
||||
this.autoSaveQueued = false;
|
||||
this.resumeAttempted = false;
|
||||
this.lastInkState = null;
|
||||
this.clientResetGeneration = 0;
|
||||
this.restoreGeneration = 0;
|
||||
this.pendingHistoryRestoreCleanup = null;
|
||||
|
||||
// Bind methods using parent's bindMethods utility
|
||||
this.bindMethods([
|
||||
@@ -52,6 +55,17 @@ class GameLoopModule extends BaseModule {
|
||||
'resetClientPlaybackAndDisplay'
|
||||
]);
|
||||
}
|
||||
|
||||
clearPendingHistoryRestore(reason = 'cancelled') {
|
||||
if (this.pendingHistoryRestoreCleanup) {
|
||||
this.pendingHistoryRestoreCleanup(reason);
|
||||
this.pendingHistoryRestoreCleanup = null;
|
||||
return;
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||
detail: { active: false, reason }
|
||||
}));
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.reportProgress(100, "Game loop initialized");
|
||||
@@ -402,6 +416,12 @@ class GameLoopModule extends BaseModule {
|
||||
if (options.resetDisplay) {
|
||||
await this.resetClientPlaybackAndDisplay();
|
||||
}
|
||||
const restoreGeneration = ++this.restoreGeneration;
|
||||
const resetGeneration = this.clientResetGeneration;
|
||||
const isCurrentRestore = () =>
|
||||
restoreGeneration === this.restoreGeneration &&
|
||||
resetGeneration === this.clientResetGeneration;
|
||||
this.clearPendingHistoryRestore(`${reason}-superseded`);
|
||||
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||
detail: { active: true, reason }
|
||||
}));
|
||||
@@ -416,10 +436,12 @@ class GameLoopModule extends BaseModule {
|
||||
const uiController = this.getModule('ui-controller');
|
||||
if (browserSave && uiController?.displayHandler?.restoreFromHistory) {
|
||||
await uiController.displayHandler.restoreFromHistory(browserSave);
|
||||
if (!isCurrentRestore()) return;
|
||||
}
|
||||
const audioManager = this.getModule('audio-manager');
|
||||
if (browserSave?.musicState && audioManager?.restoreMusicState) {
|
||||
await audioManager.restoreMusicState(browserSave.musicState);
|
||||
if (!isCurrentRestore()) return;
|
||||
}
|
||||
const hasUnrenderedHistory = this.hasUnrenderedHistory(browserSave);
|
||||
if (hasUnrenderedHistory) {
|
||||
@@ -430,19 +452,27 @@ class GameLoopModule extends BaseModule {
|
||||
}));
|
||||
}
|
||||
if (hasUnrenderedHistory) {
|
||||
await this.queueUnrenderedHistoryBlocks(browserSave);
|
||||
await this.queueUnrenderedHistoryBlocks(browserSave, isCurrentRestore);
|
||||
if (!isCurrentRestore()) return;
|
||||
}
|
||||
if (!hasUnrenderedHistory) {
|
||||
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||
detail: { active: false, reason: `${reason}-complete` }
|
||||
}));
|
||||
} else {
|
||||
const clearRestoring = () => {
|
||||
const clearRestoring = (eventOrReason = 'pending-output-drained') => {
|
||||
const clearReason = typeof eventOrReason === 'string'
|
||||
? eventOrReason
|
||||
: 'pending-output-drained';
|
||||
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||
detail: { active: false, reason: 'pending-output-drained' }
|
||||
detail: { active: false, reason: clearReason }
|
||||
}));
|
||||
document.removeEventListener('tts:queue-empty', clearRestoring);
|
||||
if (this.pendingHistoryRestoreCleanup === clearRestoring) {
|
||||
this.pendingHistoryRestoreCleanup = null;
|
||||
}
|
||||
};
|
||||
this.pendingHistoryRestoreCleanup = clearRestoring;
|
||||
document.addEventListener('tts:queue-empty', clearRestoring);
|
||||
}
|
||||
}
|
||||
@@ -522,7 +552,7 @@ class GameLoopModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
async queueUnrenderedHistoryBlocks(saveRecord = {}) {
|
||||
async queueUnrenderedHistoryBlocks(saveRecord = {}, isCurrentRestore = null) {
|
||||
const storyHistory = this.getModule('story-history');
|
||||
const textBuffer = this.getModule('text-buffer');
|
||||
if (!storyHistory || !textBuffer || typeof textBuffer.addBlocks !== 'function') return;
|
||||
@@ -530,10 +560,16 @@ class GameLoopModule extends BaseModule {
|
||||
const end = Math.max(0, Number(saveRecord.latestBlockId || 0));
|
||||
if (end < start) return;
|
||||
const blocks = await storyHistory.getBlocksRange(saveRecord.gameId, start, end);
|
||||
if (typeof isCurrentRestore === 'function' && !isCurrentRestore()) return;
|
||||
if (saveRecord.gameId && storyHistory.currentGameId !== saveRecord.gameId) return;
|
||||
textBuffer.addBlocks(blocks);
|
||||
}
|
||||
|
||||
async resetClientPlaybackAndDisplay() {
|
||||
this.clientResetGeneration += 1;
|
||||
this.restoreGeneration += 1;
|
||||
this.clearPendingHistoryRestore('client-reset');
|
||||
|
||||
const playbackCoordinator = this.getModule('playback-coordinator');
|
||||
if (playbackCoordinator && typeof playbackCoordinator.stop === 'function') {
|
||||
await playbackCoordinator.stop();
|
||||
|
||||
@@ -33,6 +33,8 @@ class SocketClientModule extends BaseModule {
|
||||
this.pendingCommand = null;
|
||||
this.gameApiTimeoutMs = GAME_API_TIMEOUT_MS;
|
||||
this.playerCommandTimeoutMs = PLAYER_COMMAND_TIMEOUT_MS;
|
||||
this.gameApiRequestId = 0;
|
||||
this.latestNarrativeRequestId = 0;
|
||||
|
||||
// Bind methods using parent's bindMethods utility
|
||||
this.bindMethods([
|
||||
@@ -220,6 +222,15 @@ class SocketClientModule extends BaseModule {
|
||||
|
||||
// Special handling for narrative text
|
||||
this.socket.on('narrativeResponse', (data) => {
|
||||
const responseRequestId = Number(data?.clientRequestId || 0);
|
||||
if (responseRequestId > 0 && responseRequestId !== this.latestNarrativeRequestId) {
|
||||
console.warn('Socket Client: Ignoring stale narrative response', {
|
||||
responseRequestId,
|
||||
latestNarrativeRequestId: this.latestNarrativeRequestId,
|
||||
turnId: data?.turnId
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.clearPendingCommand('narrative-response');
|
||||
this.processTurnResult(data);
|
||||
});
|
||||
@@ -834,6 +845,11 @@ class SocketClientModule extends BaseModule {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = ++this.gameApiRequestId;
|
||||
const normalizedMethod = String(method || '').replace(/\(\)$/, '');
|
||||
if (['newGame', 'loadGame', 'chooseChoice'].includes(normalizedMethod)) {
|
||||
this.latestNarrativeRequestId = requestId;
|
||||
}
|
||||
let settled = false;
|
||||
const finish = (response) => {
|
||||
if (settled) return;
|
||||
@@ -852,7 +868,7 @@ class SocketClientModule extends BaseModule {
|
||||
finish({ success: false, error: 'timeout', method });
|
||||
}, this.gameApiTimeoutMs);
|
||||
|
||||
this.socket.emit('gameApi', { method, args }, (response) => {
|
||||
this.socket.emit('gameApi', { method, args, requestId }, (response) => {
|
||||
finish(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2335,6 +2335,19 @@ class UIDisplayHandlerModule extends BaseModule {
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderWindowToken += 1;
|
||||
this.scrollRequestId += 1;
|
||||
if (this.scrollAnimationFrameId != null) {
|
||||
cancelAnimationFrame(this.scrollAnimationFrameId);
|
||||
this.scrollAnimationFrameId = null;
|
||||
}
|
||||
if (this.scrollAnimationResolve) {
|
||||
this.scrollAnimationResolve();
|
||||
this.scrollAnimationResolve = null;
|
||||
this.scrollAnimationPromise = null;
|
||||
}
|
||||
this.storyScrollAnimation = null;
|
||||
|
||||
if (document.documentElement.dataset.skippablePause === 'true') {
|
||||
document.dispatchEvent(new CustomEvent('ui:command', {
|
||||
detail: { moduleId: this.id, type: 'continue', source: 'display-clear' }
|
||||
|
||||
Reference in New Issue
Block a user