Add Ink session recovery and Coolify Docker support
This commit is contained in:
+138
-14
@@ -23,6 +23,13 @@ class GameLoopModule extends BaseModule {
|
||||
};
|
||||
|
||||
this.isRunning = false;
|
||||
this.autoSaveSlot = 'autosave';
|
||||
this.currentChoices = [];
|
||||
this.currentInputMode = 'none';
|
||||
this.autoSaveInProgress = false;
|
||||
this.autoSaveQueued = false;
|
||||
this.resumeAttempted = false;
|
||||
this.lastInkState = null;
|
||||
|
||||
// Bind methods using parent's bindMethods utility
|
||||
this.bindMethods([
|
||||
@@ -34,6 +41,9 @@ class GameLoopModule extends BaseModule {
|
||||
'refreshGameApiState',
|
||||
'hasSaveGame',
|
||||
'queueUnrenderedHistoryBlocks',
|
||||
'autoSaveCurrentSession',
|
||||
'restoreBrowserSave',
|
||||
'resumeAutosaveIfAvailable',
|
||||
'requestStartGame',
|
||||
'requestSaveGame',
|
||||
'requestLoadGame',
|
||||
@@ -74,6 +84,7 @@ class GameLoopModule extends BaseModule {
|
||||
document.addEventListener('ui:game:save', () => this.requestSaveGame());
|
||||
document.addEventListener('ui:game:load', () => this.requestLoadGame());
|
||||
document.addEventListener('story:input-mode', (event) => {
|
||||
this.currentInputMode = ['text', 'choice', 'end', 'none'].includes(event.detail) ? event.detail : 'none';
|
||||
if (event.detail !== 'end') {
|
||||
return;
|
||||
}
|
||||
@@ -81,6 +92,18 @@ class GameLoopModule extends BaseModule {
|
||||
this.gameState.ended = true;
|
||||
this.gameState.canSave = false;
|
||||
this.updateUIState();
|
||||
this.autoSaveCurrentSession();
|
||||
});
|
||||
document.addEventListener('story:choices', (event) => {
|
||||
this.currentChoices = Array.isArray(event.detail) ? event.detail : [];
|
||||
});
|
||||
document.addEventListener('story:turn-complete', (event) => {
|
||||
const detail = event.detail || {};
|
||||
this.currentChoices = Array.isArray(detail.choices) ? detail.choices : this.currentChoices;
|
||||
this.currentInputMode = ['text', 'choice', 'end', 'none'].includes(detail.inputMode)
|
||||
? detail.inputMode
|
||||
: this.currentInputMode;
|
||||
this.autoSaveCurrentSession();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,6 +131,10 @@ class GameLoopModule extends BaseModule {
|
||||
|
||||
this.refreshGameApiState();
|
||||
});
|
||||
|
||||
socketClient.on('disconnect', () => {
|
||||
this.resumeAttempted = false;
|
||||
});
|
||||
|
||||
// Listen for game state updates
|
||||
socketClient.on('gameStateUpdate', (data) => {
|
||||
@@ -153,6 +180,10 @@ class GameLoopModule extends BaseModule {
|
||||
]);
|
||||
|
||||
this.gameState.started = Boolean(running?.result);
|
||||
if (!this.gameState.started) {
|
||||
const resumed = await this.resumeAutosaveIfAvailable();
|
||||
if (resumed) return;
|
||||
}
|
||||
if (this.gameState.started) {
|
||||
this.gameState.startedOnce = true;
|
||||
this.gameState.ended = false;
|
||||
@@ -161,6 +192,41 @@ class GameLoopModule extends BaseModule {
|
||||
this.gameState.canLoad = Boolean(hasSave?.result);
|
||||
this.updateUIState();
|
||||
}
|
||||
|
||||
async resumeAutosaveIfAvailable() {
|
||||
if (this.resumeAttempted) return false;
|
||||
this.resumeAttempted = true;
|
||||
|
||||
const storyHistory = this.getModule('story-history');
|
||||
const socketClient = this.getModule('socket-client');
|
||||
if (!storyHistory || !socketClient || typeof storyHistory.loadSlot !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const browserSave = await storyHistory.loadSlot(this.autoSaveSlot);
|
||||
if (!browserSave?.inkState || browserSave.running === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await socketClient.resumeGame(browserSave.inkState);
|
||||
if (!response?.success) {
|
||||
console.warn('GameLoop: autosave resume failed', response);
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.restoreBrowserSave(browserSave, 'autosave-resume', { resetDisplay: true });
|
||||
this.gameState.started = Boolean(response.running);
|
||||
this.gameState.startedOnce = true;
|
||||
this.gameState.ended = !response.running && browserSave.inputMode === 'end';
|
||||
this.gameState.canSave = this.gameState.started;
|
||||
this.gameState.canLoad = true;
|
||||
this.currentChoices = Array.isArray(browserSave.choices) ? browserSave.choices : [];
|
||||
this.currentInputMode = browserSave.inputMode || 'none';
|
||||
document.dispatchEvent(new CustomEvent('story:choices', { detail: this.currentChoices }));
|
||||
document.dispatchEvent(new CustomEvent('story:input-mode', { detail: this.currentInputMode }));
|
||||
this.updateUIState();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the game state
|
||||
@@ -249,8 +315,12 @@ class GameLoopModule extends BaseModule {
|
||||
inkState: response.savedState || null,
|
||||
latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0,
|
||||
renderedLineCount: storyHistory.renderedLineCount || 0,
|
||||
musicState: audioManager?.getMusicState?.() || null
|
||||
musicState: audioManager?.getMusicState?.() || null,
|
||||
choices: this.currentChoices,
|
||||
inputMode: this.currentInputMode,
|
||||
running: this.gameState.started && !this.gameState.ended
|
||||
});
|
||||
this.lastInkState = response.savedState || this.lastInkState;
|
||||
}
|
||||
this.gameState.canLoad = true;
|
||||
this.updateUIState();
|
||||
@@ -281,9 +351,31 @@ class GameLoopModule extends BaseModule {
|
||||
this.gameState.canSave = true;
|
||||
this.gameState.canLoad = true;
|
||||
this.updateUIState();
|
||||
await this.resetClientPlaybackAndDisplay();
|
||||
await this.restoreBrowserSave(browserSave, 'load-game', { resetDisplay: true });
|
||||
const response = await socketClient.loadGame(1, browserSave?.inkState || null);
|
||||
if (response?.success && browserSave && Array.isArray(browserSave.choices)) {
|
||||
this.currentChoices = browserSave.choices;
|
||||
this.currentInputMode = browserSave.inputMode || this.currentInputMode;
|
||||
}
|
||||
if (response?.success) {
|
||||
this.gameState.started = true;
|
||||
this.gameState.startedOnce = true;
|
||||
this.gameState.ended = false;
|
||||
this.gameState.canSave = true;
|
||||
this.gameState.canLoad = true;
|
||||
this.updateUIState();
|
||||
}
|
||||
}
|
||||
|
||||
async restoreBrowserSave(browserSave, reason = 'load-game', options = {}) {
|
||||
const storyHistory = this.getModule('story-history');
|
||||
if (!browserSave || !storyHistory) return;
|
||||
|
||||
if (options.resetDisplay) {
|
||||
await this.resetClientPlaybackAndDisplay();
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||
detail: { active: true, reason: 'load-game' }
|
||||
detail: { active: true, reason }
|
||||
}));
|
||||
if (browserSave?.gameId && storyHistory?.setCurrentGame) {
|
||||
storyHistory.setCurrentGame(
|
||||
@@ -310,21 +402,12 @@ class GameLoopModule extends BaseModule {
|
||||
detail: { state: 'waiting-generating', reason: 'restoring-pending-output' }
|
||||
}));
|
||||
}
|
||||
const response = await socketClient.loadGame(1, browserSave?.inkState || null);
|
||||
if (response?.success && hasUnrenderedHistory) {
|
||||
if (hasUnrenderedHistory) {
|
||||
await this.queueUnrenderedHistoryBlocks(browserSave);
|
||||
}
|
||||
if (response?.success) {
|
||||
this.gameState.started = true;
|
||||
this.gameState.startedOnce = true;
|
||||
this.gameState.ended = false;
|
||||
this.gameState.canSave = true;
|
||||
this.gameState.canLoad = true;
|
||||
this.updateUIState();
|
||||
}
|
||||
if (!hasUnrenderedHistory) {
|
||||
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||
detail: { active: false, reason: 'load-game-complete' }
|
||||
detail: { active: false, reason: `${reason}-complete` }
|
||||
}));
|
||||
} else {
|
||||
const clearRestoring = () => {
|
||||
@@ -347,6 +430,47 @@ class GameLoopModule extends BaseModule {
|
||||
return socketClient?.hasSaveGame ? socketClient.hasSaveGame(slot) : { success: false, result: false };
|
||||
}
|
||||
|
||||
async autoSaveCurrentSession() {
|
||||
if (!this.gameState.startedOnce || this.autoSaveInProgress) {
|
||||
this.autoSaveQueued = this.autoSaveInProgress;
|
||||
return;
|
||||
}
|
||||
|
||||
const socketClient = this.getModule('socket-client');
|
||||
const storyHistory = this.getModule('story-history');
|
||||
if (!socketClient || !storyHistory || typeof storyHistory.saveSlot !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.autoSaveInProgress = true;
|
||||
try {
|
||||
const response = this.gameState.started && typeof socketClient.exportGameState === 'function'
|
||||
? await socketClient.exportGameState()
|
||||
: null;
|
||||
if (!response?.success || !response.savedState) {
|
||||
return;
|
||||
}
|
||||
this.lastInkState = response.savedState;
|
||||
|
||||
const audioManager = this.getModule('audio-manager');
|
||||
await storyHistory.saveSlot(this.autoSaveSlot, {
|
||||
inkState: response.savedState,
|
||||
latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0,
|
||||
renderedLineCount: storyHistory.renderedLineCount || 0,
|
||||
musicState: audioManager?.getMusicState?.() || null,
|
||||
choices: this.currentChoices,
|
||||
inputMode: this.currentInputMode,
|
||||
running: this.gameState.started && !this.gameState.ended
|
||||
});
|
||||
} finally {
|
||||
this.autoSaveInProgress = false;
|
||||
if (this.autoSaveQueued) {
|
||||
this.autoSaveQueued = false;
|
||||
this.autoSaveCurrentSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async queueUnrenderedHistoryBlocks(saveRecord = {}) {
|
||||
const storyHistory = this.getModule('story-history');
|
||||
const textBuffer = this.getModule('text-buffer');
|
||||
|
||||
Reference in New Issue
Block a user