Fix stale restore after game restart
This commit is contained in:
Vendored
+14
-6
@@ -112,14 +112,20 @@ function getOrCreateEngine(socketId) {
|
|||||||
}
|
}
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
async function handleGameApi(socket, method, args) {
|
function withClientRequestId(turn, requestId) {
|
||||||
|
const id = Number(requestId || 0);
|
||||||
|
return Number.isInteger(id) && id > 0
|
||||||
|
? { ...turn, clientRequestId: id }
|
||||||
|
: turn;
|
||||||
|
}
|
||||||
|
async function handleGameApi(socket, method, args, requestId) {
|
||||||
const slots = getSlots(socket.id);
|
const slots = getSlots(socket.id);
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'newGame':
|
case 'newGame':
|
||||||
case 'newGame()': {
|
case 'newGame()': {
|
||||||
const engine = new ink_engine_1.InkEngine(getStoryPath());
|
const engine = new ink_engine_1.InkEngine(getStoryPath());
|
||||||
sessions.set(socket.id, engine);
|
sessions.set(socket.id, engine);
|
||||||
socket.emit('narrativeResponse', engine.newGame());
|
socket.emit('narrativeResponse', withClientRequestId(engine.newGame(), requestId));
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
result: true,
|
result: true,
|
||||||
@@ -138,7 +144,7 @@ async function handleGameApi(socket, method, args) {
|
|||||||
if (!Number.isInteger(choiceIndex)) {
|
if (!Number.isInteger(choiceIndex)) {
|
||||||
return { success: false, error: 'invalid_choice', result: false };
|
return { success: false, error: 'invalid_choice', result: false };
|
||||||
}
|
}
|
||||||
socket.emit('narrativeResponse', engine.chooseChoice(choiceIndex));
|
socket.emit('narrativeResponse', withClientRequestId(engine.chooseChoice(choiceIndex), requestId));
|
||||||
return { success: true, result: true };
|
return { success: true, result: true };
|
||||||
}
|
}
|
||||||
case 'loadGame':
|
case 'loadGame':
|
||||||
@@ -149,8 +155,8 @@ async function handleGameApi(socket, method, args) {
|
|||||||
return { success: false, error: 'missing_save', result: false };
|
return { success: false, error: 'missing_save', result: false };
|
||||||
}
|
}
|
||||||
const engine = getOrCreateEngine(socket.id);
|
const engine = getOrCreateEngine(socket.id);
|
||||||
socket.emit('narrativeResponse', engine.loadGame(browserSave || slots.get(slot)));
|
socket.emit('narrativeResponse', withClientRequestId(engine.loadGame(browserSave || slots.get(slot)), requestId));
|
||||||
socket.emit('gameLoaded', { slot });
|
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
|
||||||
return { success: true, result: true, running: true, slot };
|
return { success: true, result: true, running: true, slot };
|
||||||
}
|
}
|
||||||
case 'resumeGame':
|
case 'resumeGame':
|
||||||
@@ -204,7 +210,9 @@ io.on('connection', (socket) => {
|
|||||||
socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig));
|
socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig));
|
||||||
socket.on('gameApi', async (request, respond) => {
|
socket.on('gameApi', async (request, respond) => {
|
||||||
try {
|
try {
|
||||||
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : []);
|
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : [], Number.isInteger(Number(request?.requestId)) && Number(request?.requestId) > 0
|
||||||
|
? Number(request?.requestId)
|
||||||
|
: undefined);
|
||||||
if (typeof respond === 'function')
|
if (typeof respond === 'function')
|
||||||
respond(result);
|
respond(result);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+15
-8
@@ -103,7 +103,13 @@ function normalizeSaveSlot(slot) {
|
|||||||
const value = Number(slot);
|
const value = Number(slot);
|
||||||
return Number.isInteger(value) && value > 0 ? value : 1;
|
return Number.isInteger(value) && value > 0 ? value : 1;
|
||||||
}
|
}
|
||||||
async function startDemoGameForSocket(socket) {
|
function withClientRequestId(turn, requestId) {
|
||||||
|
const id = Number(requestId || 0);
|
||||||
|
return Number.isInteger(id) && id > 0
|
||||||
|
? { ...turn, clientRequestId: id }
|
||||||
|
: turn;
|
||||||
|
}
|
||||||
|
async function startDemoGameForSocket(socket, requestId) {
|
||||||
nextTurnIds.set(socket.id, 1);
|
nextTurnIds.set(socket.id, 1);
|
||||||
const gameRunner = new game_runner_1.GameRunner();
|
const gameRunner = new game_runner_1.GameRunner();
|
||||||
const worldFile = (0, game_config_1.projectPath)(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile);
|
const worldFile = (0, game_config_1.projectPath)(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile);
|
||||||
@@ -114,7 +120,7 @@ async function startDemoGameForSocket(socket) {
|
|||||||
...(0, turn_result_1.textToParagraphs)(gameState.world.introduction),
|
...(0, turn_result_1.textToParagraphs)(gameState.world.introduction),
|
||||||
...(0, turn_result_1.textToParagraphs)(gameRunner.getCurrentRoomDescription()),
|
...(0, turn_result_1.textToParagraphs)(gameRunner.getCurrentRoomDescription()),
|
||||||
];
|
];
|
||||||
socket.emit('narrativeResponse', {
|
socket.emit('narrativeResponse', withClientRequestId({
|
||||||
turnId: nextTurnId(socket.id),
|
turnId: nextTurnId(socket.id),
|
||||||
paragraphs,
|
paragraphs,
|
||||||
choices: [],
|
choices: [],
|
||||||
@@ -122,16 +128,16 @@ async function startDemoGameForSocket(socket) {
|
|||||||
gameState: {
|
gameState: {
|
||||||
currentRoomId: gameState.currentRoomId,
|
currentRoomId: gameState.currentRoomId,
|
||||||
},
|
},
|
||||||
});
|
}, requestId));
|
||||||
return gameRunner;
|
return gameRunner;
|
||||||
}
|
}
|
||||||
async function handleGameApi(socket, method, args = []) {
|
async function handleGameApi(socket, method, args = [], requestId) {
|
||||||
const saveGames = socket.data.saveGames || new Map();
|
const saveGames = socket.data.saveGames || new Map();
|
||||||
socket.data.saveGames = saveGames;
|
socket.data.saveGames = saveGames;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'newGame':
|
case 'newGame':
|
||||||
case 'newGame()':
|
case 'newGame()':
|
||||||
await startDemoGameForSocket(socket);
|
await startDemoGameForSocket(socket, requestId);
|
||||||
return { success: true, result: true, running: true, canLoad: saveGames.size > 0 };
|
return { success: true, result: true, running: true, canLoad: saveGames.size > 0 };
|
||||||
case 'loadGame':
|
case 'loadGame':
|
||||||
case 'loadGame()': {
|
case 'loadGame()': {
|
||||||
@@ -139,8 +145,8 @@ async function handleGameApi(socket, method, args = []) {
|
|||||||
if (!saveGames.has(slot)) {
|
if (!saveGames.has(slot)) {
|
||||||
return { success: false, error: 'missing_save', result: false };
|
return { success: false, error: 'missing_save', result: false };
|
||||||
}
|
}
|
||||||
await startDemoGameForSocket(socket);
|
await startDemoGameForSocket(socket, requestId);
|
||||||
socket.emit('gameLoaded', { slot });
|
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
|
||||||
return { success: true, result: true, running: true, slot };
|
return { success: true, result: true, running: true, slot };
|
||||||
}
|
}
|
||||||
case 'saveGame':
|
case 'saveGame':
|
||||||
@@ -176,7 +182,8 @@ io.on('connection', (socket) => {
|
|||||||
socket.data.saveGames = new Map();
|
socket.data.saveGames = new Map();
|
||||||
socket.on('gameApi', async (request, respond) => {
|
socket.on('gameApi', async (request, respond) => {
|
||||||
try {
|
try {
|
||||||
const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : []);
|
const requestId = Number(request?.requestId || 0);
|
||||||
|
const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : [], Number.isInteger(requestId) && requestId > 0 ? requestId : undefined);
|
||||||
if (typeof respond === 'function') {
|
if (typeof respond === 'function') {
|
||||||
respond(response);
|
respond(response);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+13
-5
@@ -127,7 +127,13 @@ function getSlots(socketId) {
|
|||||||
}
|
}
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
async function handleGameApi(socket, method, args) {
|
function withClientRequestId(turn, requestId) {
|
||||||
|
const id = Number(requestId || 0);
|
||||||
|
return Number.isInteger(id) && id > 0
|
||||||
|
? { ...turn, clientRequestId: id }
|
||||||
|
: turn;
|
||||||
|
}
|
||||||
|
async function handleGameApi(socket, method, args, requestId) {
|
||||||
const slots = getSlots(socket.id);
|
const slots = getSlots(socket.id);
|
||||||
debugLog(`gameApi request from ${socket.id}: ${method}`, { args });
|
debugLog(`gameApi request from ${socket.id}: ${method}`, { args });
|
||||||
switch (method) {
|
switch (method) {
|
||||||
@@ -135,7 +141,7 @@ async function handleGameApi(socket, method, args) {
|
|||||||
case 'newGame()': {
|
case 'newGame()': {
|
||||||
const engine = getOrCreateEngine(socket.id);
|
const engine = getOrCreateEngine(socket.id);
|
||||||
const turn = await engine.newGame();
|
const turn = await engine.newGame();
|
||||||
socket.emit('narrativeResponse', toClientTurn(turn));
|
socket.emit('narrativeResponse', withClientRequestId(toClientTurn(turn), requestId));
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
result: true,
|
result: true,
|
||||||
@@ -151,8 +157,8 @@ async function handleGameApi(socket, method, args) {
|
|||||||
}
|
}
|
||||||
const engine = getOrCreateEngine(socket.id);
|
const engine = getOrCreateEngine(socket.id);
|
||||||
const turn = await engine.loadGame(slots.get(slot));
|
const turn = await engine.loadGame(slots.get(slot));
|
||||||
socket.emit('narrativeResponse', toClientTurn(turn));
|
socket.emit('narrativeResponse', withClientRequestId(toClientTurn(turn), requestId));
|
||||||
socket.emit('gameLoaded', { slot });
|
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
|
||||||
return { success: true, result: true, running: true, slot };
|
return { success: true, result: true, running: true, slot };
|
||||||
}
|
}
|
||||||
case 'saveGame':
|
case 'saveGame':
|
||||||
@@ -229,7 +235,9 @@ io.on('connection', (socket) => {
|
|||||||
socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig));
|
socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig));
|
||||||
socket.on('gameApi', async (request, respond) => {
|
socket.on('gameApi', async (request, respond) => {
|
||||||
try {
|
try {
|
||||||
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : []);
|
const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : [], Number.isInteger(Number(request?.requestId)) && Number(request?.requestId) > 0
|
||||||
|
? Number(request?.requestId)
|
||||||
|
: undefined);
|
||||||
debugLog(`gameApi response to ${socket.id}`, result);
|
debugLog(`gameApi response to ${socket.id}`, result);
|
||||||
if (typeof respond === 'function')
|
if (typeof respond === 'function')
|
||||||
respond(result);
|
respond(result);
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -30,6 +30,9 @@ class GameLoopModule extends BaseModule {
|
|||||||
this.autoSaveQueued = false;
|
this.autoSaveQueued = false;
|
||||||
this.resumeAttempted = false;
|
this.resumeAttempted = false;
|
||||||
this.lastInkState = null;
|
this.lastInkState = null;
|
||||||
|
this.clientResetGeneration = 0;
|
||||||
|
this.restoreGeneration = 0;
|
||||||
|
this.pendingHistoryRestoreCleanup = null;
|
||||||
|
|
||||||
// Bind methods using parent's bindMethods utility
|
// Bind methods using parent's bindMethods utility
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
@@ -52,6 +55,17 @@ class GameLoopModule extends BaseModule {
|
|||||||
'resetClientPlaybackAndDisplay'
|
'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() {
|
async initialize() {
|
||||||
this.reportProgress(100, "Game loop initialized");
|
this.reportProgress(100, "Game loop initialized");
|
||||||
@@ -402,6 +416,12 @@ class GameLoopModule extends BaseModule {
|
|||||||
if (options.resetDisplay) {
|
if (options.resetDisplay) {
|
||||||
await this.resetClientPlaybackAndDisplay();
|
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', {
|
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||||
detail: { active: true, reason }
|
detail: { active: true, reason }
|
||||||
}));
|
}));
|
||||||
@@ -416,10 +436,12 @@ class GameLoopModule extends BaseModule {
|
|||||||
const uiController = this.getModule('ui-controller');
|
const uiController = this.getModule('ui-controller');
|
||||||
if (browserSave && uiController?.displayHandler?.restoreFromHistory) {
|
if (browserSave && uiController?.displayHandler?.restoreFromHistory) {
|
||||||
await uiController.displayHandler.restoreFromHistory(browserSave);
|
await uiController.displayHandler.restoreFromHistory(browserSave);
|
||||||
|
if (!isCurrentRestore()) return;
|
||||||
}
|
}
|
||||||
const audioManager = this.getModule('audio-manager');
|
const audioManager = this.getModule('audio-manager');
|
||||||
if (browserSave?.musicState && audioManager?.restoreMusicState) {
|
if (browserSave?.musicState && audioManager?.restoreMusicState) {
|
||||||
await audioManager.restoreMusicState(browserSave.musicState);
|
await audioManager.restoreMusicState(browserSave.musicState);
|
||||||
|
if (!isCurrentRestore()) return;
|
||||||
}
|
}
|
||||||
const hasUnrenderedHistory = this.hasUnrenderedHistory(browserSave);
|
const hasUnrenderedHistory = this.hasUnrenderedHistory(browserSave);
|
||||||
if (hasUnrenderedHistory) {
|
if (hasUnrenderedHistory) {
|
||||||
@@ -430,19 +452,27 @@ class GameLoopModule extends BaseModule {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (hasUnrenderedHistory) {
|
if (hasUnrenderedHistory) {
|
||||||
await this.queueUnrenderedHistoryBlocks(browserSave);
|
await this.queueUnrenderedHistoryBlocks(browserSave, isCurrentRestore);
|
||||||
|
if (!isCurrentRestore()) return;
|
||||||
}
|
}
|
||||||
if (!hasUnrenderedHistory) {
|
if (!hasUnrenderedHistory) {
|
||||||
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
document.dispatchEvent(new CustomEvent('story:history-restoring', {
|
||||||
detail: { active: false, reason: `${reason}-complete` }
|
detail: { active: false, reason: `${reason}-complete` }
|
||||||
}));
|
}));
|
||||||
} else {
|
} 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', {
|
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);
|
document.removeEventListener('tts:queue-empty', clearRestoring);
|
||||||
|
if (this.pendingHistoryRestoreCleanup === clearRestoring) {
|
||||||
|
this.pendingHistoryRestoreCleanup = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
this.pendingHistoryRestoreCleanup = clearRestoring;
|
||||||
document.addEventListener('tts:queue-empty', 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 storyHistory = this.getModule('story-history');
|
||||||
const textBuffer = this.getModule('text-buffer');
|
const textBuffer = this.getModule('text-buffer');
|
||||||
if (!storyHistory || !textBuffer || typeof textBuffer.addBlocks !== 'function') return;
|
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));
|
const end = Math.max(0, Number(saveRecord.latestBlockId || 0));
|
||||||
if (end < start) return;
|
if (end < start) return;
|
||||||
const blocks = await storyHistory.getBlocksRange(saveRecord.gameId, start, end);
|
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);
|
textBuffer.addBlocks(blocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetClientPlaybackAndDisplay() {
|
async resetClientPlaybackAndDisplay() {
|
||||||
|
this.clientResetGeneration += 1;
|
||||||
|
this.restoreGeneration += 1;
|
||||||
|
this.clearPendingHistoryRestore('client-reset');
|
||||||
|
|
||||||
const playbackCoordinator = this.getModule('playback-coordinator');
|
const playbackCoordinator = this.getModule('playback-coordinator');
|
||||||
if (playbackCoordinator && typeof playbackCoordinator.stop === 'function') {
|
if (playbackCoordinator && typeof playbackCoordinator.stop === 'function') {
|
||||||
await playbackCoordinator.stop();
|
await playbackCoordinator.stop();
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class SocketClientModule extends BaseModule {
|
|||||||
this.pendingCommand = null;
|
this.pendingCommand = null;
|
||||||
this.gameApiTimeoutMs = GAME_API_TIMEOUT_MS;
|
this.gameApiTimeoutMs = GAME_API_TIMEOUT_MS;
|
||||||
this.playerCommandTimeoutMs = PLAYER_COMMAND_TIMEOUT_MS;
|
this.playerCommandTimeoutMs = PLAYER_COMMAND_TIMEOUT_MS;
|
||||||
|
this.gameApiRequestId = 0;
|
||||||
|
this.latestNarrativeRequestId = 0;
|
||||||
|
|
||||||
// Bind methods using parent's bindMethods utility
|
// Bind methods using parent's bindMethods utility
|
||||||
this.bindMethods([
|
this.bindMethods([
|
||||||
@@ -220,6 +222,15 @@ class SocketClientModule extends BaseModule {
|
|||||||
|
|
||||||
// Special handling for narrative text
|
// Special handling for narrative text
|
||||||
this.socket.on('narrativeResponse', (data) => {
|
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.clearPendingCommand('narrative-response');
|
||||||
this.processTurnResult(data);
|
this.processTurnResult(data);
|
||||||
});
|
});
|
||||||
@@ -834,6 +845,11 @@ class SocketClientModule extends BaseModule {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestId = ++this.gameApiRequestId;
|
||||||
|
const normalizedMethod = String(method || '').replace(/\(\)$/, '');
|
||||||
|
if (['newGame', 'loadGame', 'chooseChoice'].includes(normalizedMethod)) {
|
||||||
|
this.latestNarrativeRequestId = requestId;
|
||||||
|
}
|
||||||
let settled = false;
|
let settled = false;
|
||||||
const finish = (response) => {
|
const finish = (response) => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
@@ -852,7 +868,7 @@ class SocketClientModule extends BaseModule {
|
|||||||
finish({ success: false, error: 'timeout', method });
|
finish({ success: false, error: 'timeout', method });
|
||||||
}, this.gameApiTimeoutMs);
|
}, this.gameApiTimeoutMs);
|
||||||
|
|
||||||
this.socket.emit('gameApi', { method, args }, (response) => {
|
this.socket.emit('gameApi', { method, args, requestId }, (response) => {
|
||||||
finish(response);
|
finish(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2335,6 +2335,19 @@ class UIDisplayHandlerModule extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
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') {
|
if (document.documentElement.dataset.skippablePause === 'true') {
|
||||||
document.dispatchEvent(new CustomEvent('ui:command', {
|
document.dispatchEvent(new CustomEvent('ui:command', {
|
||||||
detail: { moduleId: this.id, type: 'continue', source: 'display-clear' }
|
detail: { moduleId: this.id, type: 'continue', source: 'display-clear' }
|
||||||
|
|||||||
+16
-5
@@ -97,10 +97,18 @@ function getOrCreateEngine(socketId: string): InkEngine {
|
|||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withClientRequestId<T extends object>(turn: T, requestId?: number): T {
|
||||||
|
const id = Number(requestId || 0);
|
||||||
|
return Number.isInteger(id) && id > 0
|
||||||
|
? { ...turn, clientRequestId: id }
|
||||||
|
: turn;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleGameApi(
|
async function handleGameApi(
|
||||||
socket: ReturnType<SocketIOServer['sockets']['sockets']['get']> & { id: string },
|
socket: ReturnType<SocketIOServer['sockets']['sockets']['get']> & { id: string },
|
||||||
method: string,
|
method: string,
|
||||||
args: unknown[],
|
args: unknown[],
|
||||||
|
requestId?: number,
|
||||||
): Promise<object> {
|
): Promise<object> {
|
||||||
const slots = getSlots(socket.id);
|
const slots = getSlots(socket.id);
|
||||||
|
|
||||||
@@ -109,7 +117,7 @@ async function handleGameApi(
|
|||||||
case 'newGame()': {
|
case 'newGame()': {
|
||||||
const engine = new InkEngine(getStoryPath());
|
const engine = new InkEngine(getStoryPath());
|
||||||
sessions.set(socket.id, engine);
|
sessions.set(socket.id, engine);
|
||||||
socket.emit('narrativeResponse', engine.newGame());
|
socket.emit('narrativeResponse', withClientRequestId(engine.newGame(), requestId));
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
result: true,
|
result: true,
|
||||||
@@ -129,7 +137,7 @@ async function handleGameApi(
|
|||||||
if (!Number.isInteger(choiceIndex)) {
|
if (!Number.isInteger(choiceIndex)) {
|
||||||
return { success: false, error: 'invalid_choice', result: false };
|
return { success: false, error: 'invalid_choice', result: false };
|
||||||
}
|
}
|
||||||
socket.emit('narrativeResponse', engine.chooseChoice(choiceIndex));
|
socket.emit('narrativeResponse', withClientRequestId(engine.chooseChoice(choiceIndex), requestId));
|
||||||
return { success: true, result: true };
|
return { success: true, result: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,8 +149,8 @@ async function handleGameApi(
|
|||||||
return { success: false, error: 'missing_save', result: false };
|
return { success: false, error: 'missing_save', result: false };
|
||||||
}
|
}
|
||||||
const engine = getOrCreateEngine(socket.id);
|
const engine = getOrCreateEngine(socket.id);
|
||||||
socket.emit('narrativeResponse', engine.loadGame(browserSave || slots.get(slot)!));
|
socket.emit('narrativeResponse', withClientRequestId(engine.loadGame(browserSave || slots.get(slot)!), requestId));
|
||||||
socket.emit('gameLoaded', { slot });
|
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
|
||||||
return { success: true, result: true, running: true, slot };
|
return { success: true, result: true, running: true, slot };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +214,7 @@ io.on('connection', (socket) => {
|
|||||||
socket.on(
|
socket.on(
|
||||||
'gameApi',
|
'gameApi',
|
||||||
async (
|
async (
|
||||||
request: { method?: string; args?: unknown[] },
|
request: { method?: string; args?: unknown[]; requestId?: number },
|
||||||
respond: (result: object) => void,
|
respond: (result: object) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -214,6 +222,9 @@ io.on('connection', (socket) => {
|
|||||||
socket as Parameters<typeof handleGameApi>[0],
|
socket as Parameters<typeof handleGameApi>[0],
|
||||||
String(request?.method ?? ''),
|
String(request?.method ?? ''),
|
||||||
Array.isArray(request?.args) ? request.args : [],
|
Array.isArray(request?.args) ? request.args : [],
|
||||||
|
Number.isInteger(Number(request?.requestId)) && Number(request?.requestId) > 0
|
||||||
|
? Number(request?.requestId)
|
||||||
|
: undefined,
|
||||||
);
|
);
|
||||||
if (typeof respond === 'function') respond(result);
|
if (typeof respond === 'function') respond(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+21
-8
@@ -87,7 +87,14 @@ function normalizeSaveSlot(slot: unknown): number {
|
|||||||
return Number.isInteger(value) && value > 0 ? value : 1;
|
return Number.isInteger(value) && value > 0 ? value : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startDemoGameForSocket(socket: any): Promise<GameRunner> {
|
function withClientRequestId<T extends object>(turn: T, requestId?: number): T {
|
||||||
|
const id = Number(requestId || 0);
|
||||||
|
return Number.isInteger(id) && id > 0
|
||||||
|
? { ...turn, clientRequestId: id }
|
||||||
|
: turn;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDemoGameForSocket(socket: any, requestId?: number): Promise<GameRunner> {
|
||||||
nextTurnIds.set(socket.id, 1);
|
nextTurnIds.set(socket.id, 1);
|
||||||
const gameRunner = new GameRunner();
|
const gameRunner = new GameRunner();
|
||||||
const worldFile = projectPath(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile);
|
const worldFile = projectPath(process.env.DEFAULT_WORLD_FILE || engineConfig.paths.mainGameFile);
|
||||||
@@ -100,7 +107,7 @@ async function startDemoGameForSocket(socket: any): Promise<GameRunner> {
|
|||||||
...textToParagraphs(gameState.world.introduction),
|
...textToParagraphs(gameState.world.introduction),
|
||||||
...textToParagraphs(gameRunner.getCurrentRoomDescription()),
|
...textToParagraphs(gameRunner.getCurrentRoomDescription()),
|
||||||
];
|
];
|
||||||
socket.emit('narrativeResponse', {
|
socket.emit('narrativeResponse', withClientRequestId({
|
||||||
turnId: nextTurnId(socket.id),
|
turnId: nextTurnId(socket.id),
|
||||||
paragraphs,
|
paragraphs,
|
||||||
choices: [],
|
choices: [],
|
||||||
@@ -108,19 +115,19 @@ async function startDemoGameForSocket(socket: any): Promise<GameRunner> {
|
|||||||
gameState: {
|
gameState: {
|
||||||
currentRoomId: gameState.currentRoomId,
|
currentRoomId: gameState.currentRoomId,
|
||||||
},
|
},
|
||||||
});
|
}, requestId));
|
||||||
|
|
||||||
return gameRunner;
|
return gameRunner;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGameApi(socket: any, method: string, args: unknown[] = []) {
|
async function handleGameApi(socket: any, method: string, args: unknown[] = [], requestId?: number) {
|
||||||
const saveGames: Map<number, any> = socket.data.saveGames || new Map<number, any>();
|
const saveGames: Map<number, any> = socket.data.saveGames || new Map<number, any>();
|
||||||
socket.data.saveGames = saveGames;
|
socket.data.saveGames = saveGames;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'newGame':
|
case 'newGame':
|
||||||
case 'newGame()':
|
case 'newGame()':
|
||||||
await startDemoGameForSocket(socket);
|
await startDemoGameForSocket(socket, requestId);
|
||||||
return { success: true, result: true, running: true, canLoad: saveGames.size > 0 };
|
return { success: true, result: true, running: true, canLoad: saveGames.size > 0 };
|
||||||
|
|
||||||
case 'loadGame':
|
case 'loadGame':
|
||||||
@@ -129,8 +136,8 @@ async function handleGameApi(socket: any, method: string, args: unknown[] = [])
|
|||||||
if (!saveGames.has(slot)) {
|
if (!saveGames.has(slot)) {
|
||||||
return { success: false, error: 'missing_save', result: false };
|
return { success: false, error: 'missing_save', result: false };
|
||||||
}
|
}
|
||||||
await startDemoGameForSocket(socket);
|
await startDemoGameForSocket(socket, requestId);
|
||||||
socket.emit('gameLoaded', { slot });
|
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
|
||||||
return { success: true, result: true, running: true, slot };
|
return { success: true, result: true, running: true, slot };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +181,13 @@ io.on('connection', (socket) => {
|
|||||||
|
|
||||||
socket.on('gameApi', async (request, respond) => {
|
socket.on('gameApi', async (request, respond) => {
|
||||||
try {
|
try {
|
||||||
const response = await handleGameApi(socket, String(request?.method || ''), Array.isArray(request?.args) ? request.args : []);
|
const requestId = Number(request?.requestId || 0);
|
||||||
|
const response = await handleGameApi(
|
||||||
|
socket,
|
||||||
|
String(request?.method || ''),
|
||||||
|
Array.isArray(request?.args) ? request.args : [],
|
||||||
|
Number.isInteger(requestId) && requestId > 0 ? requestId : undefined,
|
||||||
|
);
|
||||||
if (typeof respond === 'function') {
|
if (typeof respond === 'function') {
|
||||||
respond(response);
|
respond(response);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-4
@@ -114,12 +114,20 @@ function getSlots(socketId: string): Map<number, string> {
|
|||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withClientRequestId<T extends object>(turn: T, requestId?: number): T {
|
||||||
|
const id = Number(requestId || 0);
|
||||||
|
return Number.isInteger(id) && id > 0
|
||||||
|
? { ...turn, clientRequestId: id }
|
||||||
|
: turn;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleGameApi(
|
async function handleGameApi(
|
||||||
socket: ReturnType<SocketIOServer['sockets']['sockets']['get']> & {
|
socket: ReturnType<SocketIOServer['sockets']['sockets']['get']> & {
|
||||||
id: string;
|
id: string;
|
||||||
},
|
},
|
||||||
method: string,
|
method: string,
|
||||||
args: unknown[],
|
args: unknown[],
|
||||||
|
requestId?: number,
|
||||||
): Promise<object> {
|
): Promise<object> {
|
||||||
const slots = getSlots(socket.id);
|
const slots = getSlots(socket.id);
|
||||||
debugLog(`gameApi request from ${socket.id}: ${method}`, { args });
|
debugLog(`gameApi request from ${socket.id}: ${method}`, { args });
|
||||||
@@ -129,7 +137,7 @@ async function handleGameApi(
|
|||||||
case 'newGame()': {
|
case 'newGame()': {
|
||||||
const engine = getOrCreateEngine(socket.id);
|
const engine = getOrCreateEngine(socket.id);
|
||||||
const turn = await engine.newGame();
|
const turn = await engine.newGame();
|
||||||
socket.emit('narrativeResponse', toClientTurn(turn));
|
socket.emit('narrativeResponse', withClientRequestId(toClientTurn(turn), requestId));
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
result: true,
|
result: true,
|
||||||
@@ -146,8 +154,8 @@ async function handleGameApi(
|
|||||||
}
|
}
|
||||||
const engine = getOrCreateEngine(socket.id);
|
const engine = getOrCreateEngine(socket.id);
|
||||||
const turn = await engine.loadGame(slots.get(slot)!);
|
const turn = await engine.loadGame(slots.get(slot)!);
|
||||||
socket.emit('narrativeResponse', toClientTurn(turn));
|
socket.emit('narrativeResponse', withClientRequestId(toClientTurn(turn), requestId));
|
||||||
socket.emit('gameLoaded', { slot });
|
socket.emit('gameLoaded', { slot, clientRequestId: requestId });
|
||||||
return { success: true, result: true, running: true, slot };
|
return { success: true, result: true, running: true, slot };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +244,7 @@ io.on('connection', (socket) => {
|
|||||||
socket.on(
|
socket.on(
|
||||||
'gameApi',
|
'gameApi',
|
||||||
async (
|
async (
|
||||||
request: { method?: string; args?: unknown[] },
|
request: { method?: string; args?: unknown[]; requestId?: number },
|
||||||
respond: (result: object) => void,
|
respond: (result: object) => void,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -244,6 +252,9 @@ io.on('connection', (socket) => {
|
|||||||
socket as Parameters<typeof handleGameApi>[0],
|
socket as Parameters<typeof handleGameApi>[0],
|
||||||
String(request?.method ?? ''),
|
String(request?.method ?? ''),
|
||||||
Array.isArray(request?.args) ? request.args : [],
|
Array.isArray(request?.args) ? request.args : [],
|
||||||
|
Number.isInteger(Number(request?.requestId)) && Number(request?.requestId) > 0
|
||||||
|
? Number(request?.requestId)
|
||||||
|
: undefined,
|
||||||
);
|
);
|
||||||
debugLog(`gameApi response to ${socket.id}`, result);
|
debugLog(`gameApi response to ${socket.id}`, result);
|
||||||
if (typeof respond === 'function') respond(result);
|
if (typeof respond === 'function') respond(result);
|
||||||
|
|||||||
Reference in New Issue
Block a user