"use strict"; /** * Ink Engine Server * * Serves the shared client UI and runs a compiled Ink JSON story through the * unified TurnResult socket protocol. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.io = exports.server = exports.app = void 0; exports.startServer = startServer; const path_1 = __importDefault(require("path")); const http_1 = __importDefault(require("http")); const express_1 = __importDefault(require("express")); const socket_io_1 = require("socket.io"); const dotenv = __importStar(require("dotenv")); const fs_1 = require("fs"); const ink_engine_1 = require("./engine/ink-engine"); const game_config_1 = require("./config/game-config"); dotenv.config(); const app = (0, express_1.default)(); exports.app = app; const server = http_1.default.createServer(app); exports.server = server; const io = new socket_io_1.Server(server); exports.io = io; const DEFAULT_PORT = 3003; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : DEFAULT_PORT; const PORT_RANGE = 300; const engineConfig = (0, game_config_1.loadGameConfig)(process.env.INK_CONFIG_FILE || './config/engines/ink.json', 'ink'); app.use(express_1.default.static(path_1.default.join(__dirname, '../public'), { etag: false, lastModified: false, setHeaders: (res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); }, })); app.get('/api/game-config', (_req, res) => { res.json((0, game_config_1.clientGameConfig)(engineConfig)); }); const sessions = new Map(); const saveSlots = new Map(); function normalizeSaveSlot(slot) { const n = Number(slot); return Number.isInteger(n) && n > 0 ? n : 1; } function getStoryPath() { return (0, game_config_1.projectPath)(process.env.INK_STORY_FILE || engineConfig.paths.inkCompiled || engineConfig.paths.mainGameFile); } function getSourcePath() { return (0, game_config_1.projectPath)(process.env.INK_SOURCE_FILE || engineConfig.paths.inkSource || ''); } function compileConfiguredStory() { const sourcePath = getSourcePath(); const outputPath = getStoryPath(); const result = (0, ink_engine_1.compileInkSource)(sourcePath, outputPath); console.log(`[ink] Compiled ${result.sourcePath} -> ${result.outputPath}` + (result.warningCount > 0 ? ` (${result.warningCount} warnings)` : '')); } function getSlots(socketId) { let slots = saveSlots.get(socketId); if (!slots) { slots = new Map(); saveSlots.set(socketId, slots); } return slots; } function getOrCreateEngine(socketId) { let engine = sessions.get(socketId); if (!engine) { engine = new ink_engine_1.InkEngine(getStoryPath()); sessions.set(socketId, engine); } return engine; } async function handleGameApi(socket, method, args) { const slots = getSlots(socket.id); switch (method) { case 'newGame': case 'newGame()': { const engine = new ink_engine_1.InkEngine(getStoryPath()); sessions.set(socket.id, engine); socket.emit('narrativeResponse', engine.newGame()); return { success: true, result: true, running: true, canLoad: slots.size > 0 }; } case 'chooseChoice': case 'chooseChoice()': { const engine = sessions.get(socket.id); if (!engine?.isRunning()) { return { success: false, error: 'game_not_running', result: false }; } const choiceIndex = Number(args[0]); if (!Number.isInteger(choiceIndex)) { return { success: false, error: 'invalid_choice', result: false }; } socket.emit('narrativeResponse', engine.chooseChoice(choiceIndex)); return { success: true, result: true }; } case 'loadGame': case 'loadGame()': { const slot = normalizeSaveSlot(args[0]); const browserSave = typeof args[1] === 'string' ? args[1] : null; if (!browserSave && !slots.has(slot)) { return { success: false, error: 'missing_save', result: false }; } const engine = getOrCreateEngine(socket.id); socket.emit('narrativeResponse', engine.loadGame(browserSave || slots.get(slot))); socket.emit('gameLoaded', { slot }); return { success: true, result: true, running: true, slot }; } case 'resumeGame': case 'resumeGame()': { const browserSave = typeof args[0] === 'string' ? args[0] : null; if (!browserSave) { return { success: false, error: 'missing_state', result: false }; } const engine = new ink_engine_1.InkEngine(getStoryPath()); engine.resumeGame(browserSave); sessions.set(socket.id, engine); return { success: true, result: true, running: engine.isRunning() }; } case 'exportGameState': case 'exportGameState()': { const engine = sessions.get(socket.id); if (!engine?.isRunning()) { return { success: false, error: 'game_not_running', result: false }; } return { success: true, result: true, savedState: engine.saveGame() }; } case 'saveGame': case 'saveGame()': { const engine = sessions.get(socket.id); if (!engine?.isRunning()) { return { success: false, error: 'game_not_running', result: false }; } const slot = normalizeSaveSlot(args[0]); const savedState = engine.saveGame(); slots.set(slot, savedState); socket.emit('gameSaved', { slot }); return { success: true, result: true, slot, savedState }; } case 'hasSaveGame': case 'hasSaveGame()': { const slot = normalizeSaveSlot(args[0]); return { success: true, result: slots.has(slot), slot }; } case 'getSaveGames': case 'getSaveGames()': return { success: true, result: Array.from(slots.keys()).sort((a, b) => a - b) }; case 'isGameRunning': case 'isGameRunning()': return { success: true, result: sessions.get(socket.id)?.isRunning() ?? false }; default: return { success: false, error: `unknown_method:${method}` }; } } io.on('connection', (socket) => { console.log(`[ink] Client connected: ${socket.id}`); socket.emit('gameConfig', (0, game_config_1.clientGameConfig)(engineConfig)); socket.on('gameApi', async (request, respond) => { try { const result = await handleGameApi(socket, String(request?.method ?? ''), Array.isArray(request?.args) ? request.args : []); if (typeof respond === 'function') respond(result); } catch (error) { console.error('[ink] gameApi error:', error); if (typeof respond === 'function') { respond({ success: false, error: error instanceof Error ? error.message : String(error), }); } } }); socket.on('disconnect', () => { console.log(`[ink] Client disconnected: ${socket.id}`); sessions.delete(socket.id); saveSlots.delete(socket.id); }); }); function ensureDirectories() { const dirs = [ path_1.default.join(__dirname, '../public'), path_1.default.join(__dirname, '../public/js'), path_1.default.join(__dirname, '../public/css'), path_1.default.join(__dirname, '../public/images'), path_1.default.join(__dirname, '../public/music'), path_1.default.join(__dirname, '../public/sounds'), path_1.default.join(__dirname, '../public/fonts'), ]; for (const dir of dirs) { if (!(0, fs_1.existsSync)(dir)) (0, fs_1.mkdirSync)(dir, { recursive: true }); } (0, game_config_1.ensureConfiguredAssetDirectories)(engineConfig); } function ensureKokoroJs() { const source = path_1.default.join(__dirname, '../node_modules/kokoro-js/dist/index.js'); const destination = path_1.default.join(__dirname, '../public/js/kokoro-js.js'); if ((0, fs_1.existsSync)(source) && !(0, fs_1.existsSync)(destination)) { (0, fs_1.copyFileSync)(source, destination); } } async function startServer(initialPort, range) { ensureDirectories(); try { ensureKokoroJs(); } catch { /* optional */ } compileConfiguredStory(); if (!(0, fs_1.existsSync)(getStoryPath())) { console.error(`[ink] Story file missing: ${getStoryPath()}`); console.error('[ink] Set INK_SOURCE_FILE or configure paths.inkSource in config/engines/ink.json.'); } let port = initialPort; while (port < initialPort + range) { try { await new Promise((resolve, reject) => { server.removeAllListeners('error'); server.removeAllListeners('listening'); server.once('listening', () => { console.log(`[ink] Ink server running on http://localhost:${port}`); resolve(); }); server.once('error', (error) => { if (error.code === 'EADDRINUSE' || error.code === 'EACCES') { console.log(`Port ${port} unavailable (${error.code}), trying ${port + 1}...`); server.close(); port++; reject(); } else { reject(error); } }); server.listen(port); }); return; } catch { if (port >= initialPort + range - 1) { throw new Error(`Failed to start server on ports ${initialPort} to ${initialPort + range - 1}`); } } } } if (require.main === module) { startServer(PORT, PORT_RANGE).catch((error) => { console.error('[ink] Failed to start:', error); process.exit(1); }); } //# sourceMappingURL=server-ink.js.map