diff --git a/server/src/api/controllers/sessionController.ts b/server/src/api/controllers/sessionController.ts index 6b8cf7e..81dcf77 100644 --- a/server/src/api/controllers/sessionController.ts +++ b/server/src/api/controllers/sessionController.ts @@ -22,7 +22,7 @@ function sessionSummary(session: ISession) { function sessionDetail(session: ISession) { return { ...sessionSummary(session), - stages: session.events, + events: session.completedEvents, } } diff --git a/server/src/game/commandHandler.ts b/server/src/game/commandHandler.ts deleted file mode 100644 index 029dce1..0000000 --- a/server/src/game/commandHandler.ts +++ /dev/null @@ -1,202 +0,0 @@ -import minimist from 'minimist' -import { HydratedDocument } from 'mongoose' -import { FileGraph } from '@linux-odyssey/file-graph' -import { ICondition, Quest, Stage, IException } from '@linux-odyssey/game' -import { ICommand, ISession } from '@linux-odyssey/models' -import { pushToSession } from '../api/socket.js' -import SessionHandler, { ExecuteResult } from './sessionHandler.js' -import { checkFile } from '../containers/cli.js' -import logger from '../utils/logger.js' - -// 檢查 pattern 是否符合 input -const checkMatch = (pattern?: string[], input?: string) => { - if (!pattern || pattern.length === 0) return true - return pattern.some((p) => { - const regex = new RegExp(p) - return input && regex.test(input) - }) -} - -export default class CommandHandler extends SessionHandler { - commandInput: ICommand - argv: minimist.ParsedArgs - params: Record - - constructor( - session: HydratedDocument, - quest: Quest, - commandInput: ICommand, - params: Record - ) { - super(session, quest) - this.commandInput = commandInput - this.argv = minimist(this.commandInput.command.split(' ')) - - this.params = params - } - - handleEvent() { - if (this.params.discover) this.discoverHandler() - } - - handleCommand() { - const command = this.argv._[0] - switch (command) { - case 'cd': - pushToSession(this.session.id, 'graph', { - pwd: this.commandInput.pwd, - }) - break - - case 'ls': - pushToSession(this.session.id, 'graph', { - pwd: this.commandInput.pwd, - discover: this.params.discover, - }) - break - - default: - break - } - } - - async discoverHandler() { - try { - const graph = new FileGraph(this.session.graph) - graph.discover(this.params.discover) - this.session.graph = graph - } catch (e) { - logger.error('Update file graph failed', { - error: (e as Error).message, - session: this.session.id, - graph: this.session.graph, - discover: this.params.discover, - }) - } - } - - async isMatch(condition: Condition): Promise { - return ( - this.checkKeys(condition) && - (await this.checkFiles(condition.files)) && - (await this.checkNot(condition.not)) && - (await this.checkOr(condition.or)) === true - ) - } - - checkKeys(condition: Condition) { - return ( - checkMatch(condition.command, this.commandInput.command) && - checkMatch(condition.output, this.commandInput.output) && - checkMatch(condition.error, this.commandInput.error) && - checkMatch(condition.pwd, this.commandInput.pwd) - ) - } - - async checkFiles(files?: FileCondition[]): Promise { - if (!files || files.length === 0) { - return true - } - try { - const checks = await Promise.all( - files.map( - (f) => - this.session.containerId && checkFile(this.session.containerId, f) - ) - ) - return checks.every((c) => c === true) - } catch { - return false - } - } - - async checkNot(condition?: Condition): Promise { - if (!condition) { - return true - } - return !(await this.isMatch(condition)) - } - - async checkOr(conditions?: Condition[]): Promise { - if (!conditions || conditions.length === 0) { - return true - } - const matches = await Promise.all(conditions.map((c) => this.isMatch(c))) - return matches.some((m) => m === true) - } - - async checkException(stage: Stage): Promise { - const exceptions = stage.exceptions || [] - for (const exception of exceptions) { - // eslint-disable-next-line no-await-in-loop - if (exception.condition && (await this.isMatch(exception.condition))) { - return exception - } - if (exception.catchAll) return exception - } - return null - } - - async run(): Promise { - try { - const stages = this.getStages() - if (stages.length === 0) { - logger.warn('stage not found', { session: this.session.id }) - return null - } - - this.handleEvent() - this.handleCommand() - - const matches = await Promise.all( - stages.map((stage) => stage.condition && this.isMatch(stage.condition)) - ) - - const stage = stages.find((_, i) => matches[i]) - if (stage) { - const response = this.execute(stage) - - return { - stage: stage.id, - end: this.session.status === 'finished', - ...response, - } - } - - // stage exception - for (const s of stages) { - // eslint-disable-next-line no-await-in-loop - const exception = await this.checkException(s) - if (exception) { - return this.executeException(exception) - } - } - - // global exception - if (this.quest.exceptions.length > 0) { - const globalExceptions = this.getUnlockedComponents( - this.quest.exceptions - ) - for (const exception of globalExceptions) { - if ( - exception.condition && - // eslint-disable-next-line no-await-in-loop - (await this.isMatch(exception.condition)) - ) { - return this.executeException(exception) - } - } - } - - return null - } catch (err) { - logger.error('CommandHandler error', { - error: (err as Error).message, - session: this.session._id, - quest: this.quest.id, - command: this.commandInput, - }) - return null - } - } -} diff --git a/server/src/game/sessionHandler.ts b/server/src/game/sessionHandler.ts deleted file mode 100644 index 5a26798..0000000 --- a/server/src/game/sessionHandler.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { HydratedDocument } from 'mongoose' -import type { - Quest, - Response, - Requirement, - Stage, - Task, - Exception, -} from '@linux-odyssey/constants' -import { ISession } from '@linux-odyssey/models' - -export interface ExecuteResult { - responses: Response[] - status: string - stage?: string - end?: boolean -} - -export default class SessionHandler { - session: HydratedDocument - quest: Quest - - constructor(session: HydratedDocument, quest: Quest) { - this.session = session - this.quest = quest - } - - getSession(): HydratedDocument { - return this.session - } - - getTask(stage: Partial): Task | undefined { - return this.session.tasks.find((t) => t.id === stage.id) - } - - isCompleted(stage: Partial): boolean { - const task = this.getTask(stage) - return task ? task.completed : false - } - - getUnlockedComponents( - components: T[] - ): T[] { - return components.filter((component) => - component.requirements.every((r: string) => this.isCompleted({ id: r })) - ) - } - - getStages() { - const unlockedStages = this.getUnlockedComponents(this.quest.stages) - return unlockedStages.filter((s) => !this.isCompleted(s)) - } - - getNewTasks() { - const stages = this.getStages() - return stages - .filter((s) => s.task) - .filter((s) => !this.session.tasks.some((t) => t.id === s.id)) - .map((s) => ({ id: s.id, name: s.task, completed: false })) - } - - addNewTasks() { - const newTasks = this.getNewTasks() - this.session.tasks.push(...newTasks) - } - - execute(stage: Stage): ExecuteResult { - this.session.tasks - .filter((t) => t.id === stage.id) - .forEach((t) => { - t.completed = true - }) - - this.addNewTasks() - - if (stage.id === 'END') { - this.session.status = 'finished' - } - - return { - responses: stage.responses, - status: this.session.status, - } - } - - executeException(exception: Exception): ExecuteResult { - const { responses } = exception - - return { - responses, - status: this.session.status, - } - } -} diff --git a/server/src/models/sessionManager.ts b/server/src/models/sessionManager.ts index bf0d7ba..c4d0415 100644 --- a/server/src/models/sessionManager.ts +++ b/server/src/models/sessionManager.ts @@ -49,7 +49,7 @@ export async function createNewSession( const newSession = new Session({ user, - quest, + quest: quest.id, containerId: container.id, }) @@ -70,6 +70,7 @@ export async function createNewSession( progress.sessions.push(newSession._id) } await userProfile.save() + await newSession.save() return newSession } diff --git a/server/src/routers/sessionRouter.ts b/server/src/routers/sessionRouter.ts index 8a4d91c..9277103 100644 --- a/server/src/routers/sessionRouter.ts +++ b/server/src/routers/sessionRouter.ts @@ -34,7 +34,7 @@ export const sessionRouter = router({ status: session.status, createdAt: session.createdAt, lastActivityAt: session.lastActivityAt, - stages: session.events, + completedEvents: session.completedEvents, } } throw new TRPCError({