diff --git a/server/src/controllers/game/CoyoteGameController.ts b/server/src/controllers/game/CoyoteGameController.ts index 729f81a..c237c83 100644 --- a/server/src/controllers/game/CoyoteGameController.ts +++ b/server/src/controllers/game/CoyoteGameController.ts @@ -22,6 +22,7 @@ export interface CoyoteGameEvents { close: []; strengthChanged: [strength: GameStrengthInfo]; strengthConfigUpdated: [config: GameStrengthConfig]; + identifiersUpdated: [identifiers: string[]]; clientConnected: []; clientDisconnected: []; gameStarted: []; diff --git a/server/src/managers/CoyoteGameManager.ts b/server/src/managers/CoyoteGameManager.ts index b2d69de..eb3f53d 100644 --- a/server/src/managers/CoyoteGameManager.ts +++ b/server/src/managers/CoyoteGameManager.ts @@ -1,9 +1,9 @@ -import { EventEmitter } from "events"; import { CoyoteGameController } from "../controllers/game/CoyoteGameController"; import { DGLabWSClient } from "../controllers/ws/DGLabWS"; import { DGLabWSManager } from "./DGLabWSManager"; import { LRUCache } from "lru-cache"; import { ExEventEmitter } from "../utils/ExEventEmitter"; +import { MultipleLinkedMap } from "../utils/MultipleLinkedMap"; export interface CoyoteGameManagerEvents { @@ -13,6 +13,7 @@ export class CoyoteGameManager { private static _instance: CoyoteGameManager; private games: Map; + private gameIdentifiers: MultipleLinkedMap = new MultipleLinkedMap(); private events = new ExEventEmitter(); @@ -56,6 +57,11 @@ export class CoyoteGameManager { game.once('close', () => { this.games.delete(clientId); + this.gameIdentifiers.removeField(clientId); + }); + + game.on('identifiersUpdated', (newIdentifiers) => { + this.gameIdentifiers.setFieldValues(clientId, newIdentifiers); }); this.games.set(clientId, game); @@ -63,8 +69,18 @@ export class CoyoteGameManager { return game; } - public getGame(clientId: string) { - return this.games.get(clientId); + public getGame(id: string, identifyType: 'id' | 'readonly' | 'gameplay' = 'id') { + if (identifyType === 'id') { + return this.games.get(id); + } + + // 根据不同的标识类型查找游戏ID + const gameId = this.gameIdentifiers.getFieldKey(`${identifyType}:${id}`); + if (!gameId) { + return undefined; + } + + return this.games.get(gameId); } public async getOrCreateGame(clientId: string) { diff --git a/server/src/model/config/CustomPulseConfigUpdater.ts b/server/src/model/config/CustomPulseConfigUpdater.ts new file mode 100644 index 0000000..3650875 --- /dev/null +++ b/server/src/model/config/CustomPulseConfigUpdater.ts @@ -0,0 +1,12 @@ +import { ObjectUpdater } from "../../utils/ObjectUpdater"; +import { GameCustomPulseConfig } from "../../types/game"; + +export class CustomPulseConfigUpdater extends ObjectUpdater { + protected registerSchemas(): void { + this.addSchema(0, (obj) => obj, () => { + return { + customPulseList: [], + } as GameCustomPulseConfig; + }); + } +} \ No newline at end of file diff --git a/server/src/model/config/GamePlayConfigUpdater.ts b/server/src/model/config/GamePlayConfigUpdater.ts new file mode 100644 index 0000000..851b305 --- /dev/null +++ b/server/src/model/config/GamePlayConfigUpdater.ts @@ -0,0 +1,12 @@ +import { ObjectUpdater } from "../../utils/ObjectUpdater"; +import { CoyoteGamePlayConfig } from "../../types/gamePlay"; + +export class GamePlayConfigUpdater extends ObjectUpdater { + protected registerSchemas(): void { + this.addSchema(0, (obj) => obj, () => { + return { + gamePlayList: [], + } as CoyoteGamePlayConfig; + }); + } +} \ No newline at end of file diff --git a/server/src/model/config/GamePlayUserConfigUpdater.ts b/server/src/model/config/GamePlayUserConfigUpdater.ts new file mode 100644 index 0000000..603e13a --- /dev/null +++ b/server/src/model/config/GamePlayUserConfigUpdater.ts @@ -0,0 +1,12 @@ +import { ObjectUpdater } from "../../utils/ObjectUpdater"; +import { CoyoteGamePlayUserConfig } from "../../types/gamePlay"; + +export class GamePlayUserConfigUpdater extends ObjectUpdater { + protected registerSchemas(): void { + this.addSchema(0, (obj) => obj, () => { + return { + configList: {}, + } as CoyoteGamePlayUserConfig; + }); + } +} \ No newline at end of file diff --git a/server/src/model/config/MainGameConfigUpdater.ts b/server/src/model/config/MainGameConfigUpdater.ts new file mode 100644 index 0000000..335dc0d --- /dev/null +++ b/server/src/model/config/MainGameConfigUpdater.ts @@ -0,0 +1,18 @@ +import { DGLabPulseService } from "../../services/DGLabPulse"; +import { MainGameConfig } from "../../types/game"; +import { ObjectUpdater } from "../../utils/ObjectUpdater"; + +export class MainGameConfigUpdater extends ObjectUpdater { + protected registerSchemas(): void { + this.addSchema(0, (obj) => obj, () => { + return { + strengthChangeInterval: [15, 30], + enableBChannel: false, + bChannelStrengthMultiplier: 1, + pulseId: DGLabPulseService.instance.getDefaultPulse().id, + pulseMode: 'single', + pulseChangeInterval: 60, + } as MainGameConfig; + }); + } +} \ No newline at end of file diff --git a/server/src/services/CoyoteGameConfigService.ts b/server/src/services/CoyoteGameConfigService.ts index 9d55e6d..bcc8207 100644 --- a/server/src/services/CoyoteGameConfigService.ts +++ b/server/src/services/CoyoteGameConfigService.ts @@ -6,6 +6,11 @@ import { ExEventEmitter } from "../utils/ExEventEmitter"; import { GameCustomPulseConfig, MainGameConfig } from '../types/game'; import { DGLabPulseService } from './DGLabPulse'; import { CoyoteGamePlayConfig, CoyoteGamePlayUserConfig } from '../types/gamePlay'; +import { MainGameConfigUpdater } from '../model/config/MainGameConfigUpdater'; +import { CustomPulseConfigUpdater } from '../model/config/CustomPulseConfigUpdater'; +import { GamePlayConfigUpdater } from '../model/config/GamePlayConfigUpdater'; +import { GamePlayUserConfigUpdater } from '../model/config/GamePlayUserConfigUpdater'; +import { ObjectUpdater } from '../utils/ObjectUpdater'; export enum GameConfigType { MainGame = 'main-game', @@ -25,19 +30,19 @@ export type GameConfigTypeMap = { [GameConfigType.GamePlayUserConfig]: CoyoteGamePlayUserConfig, }; -export const CoyoteGameConfigList = [ - GameConfigType.MainGame, - GameConfigType.CustomPulse, - GameConfigType.GamePlay, - GameConfigType.GamePlayUserConfig, -]; - export class CoyoteGameConfigService { private static _instance: CoyoteGameConfigService; private gameConfigDir = 'data/game-config'; public events = new ExEventEmitter(); + + public configUpdaters: Record = { + [GameConfigType.MainGame]: new MainGameConfigUpdater(), + [GameConfigType.CustomPulse]: new CustomPulseConfigUpdater(), + [GameConfigType.GamePlay]: new GamePlayConfigUpdater(), + [GameConfigType.GamePlayUserConfig]: new GamePlayUserConfigUpdater(), + }; private configCache: LRUCache = new LRUCache({ max: 1000, @@ -61,39 +66,25 @@ export class CoyoteGameConfigService { } } - public getDefaultConfig(type: GameConfigType) { - switch (type) { - case GameConfigType.MainGame: - return { - strengthChangeInterval: [15, 30], - enableBChannel: false, - bChannelStrengthMultiplier: 1, - pulseId: DGLabPulseService.instance.getDefaultPulse().id, - pulseMode: 'single', - pulseChangeInterval: 60, - } as MainGameConfig; - case GameConfigType.CustomPulse: - return { - customPulseList: [], - } as GameCustomPulseConfig; - case GameConfigType.GamePlay: - return { - gamePlayList: [], - } as CoyoteGamePlayConfig; - case GameConfigType.GamePlayUserConfig: - return { - configList: {}, - } as CoyoteGamePlayUserConfig; - default: - return {}; - } + public getDefaultConfig(type: GameConfigType | string) { + return this.configUpdaters[type]?.getDefaultEmptyObject(); } public async set(clientId: string, type: GameConfigType, newConfig: GameConfigTypeMap[TKey]) { + const configUpdater = this.configUpdaters[type]; + if (!configUpdater) { + throw new Error(`Config type not found: ${type}`); + } + const cacheKey = `${clientId}/${type}`; this.configCache.set(cacheKey, newConfig); - await fs.promises.writeFile(path.join(this.gameConfigDir, `${clientId}.${type}.json`), JSON.stringify(newConfig, null, 4), { encoding: 'utf-8' }); + let storeConfig = { + ...newConfig, + version: configUpdater.getCurrentVersion(), + }; + + await fs.promises.writeFile(path.join(this.gameConfigDir, `${clientId}.${type}.json`), JSON.stringify(storeConfig, null, 4), { encoding: 'utf-8' }); this.events.emitSub('configUpdated', cacheKey, type, newConfig); this.events.emitSub('configUpdated', clientId, type, newConfig); @@ -107,18 +98,31 @@ export class CoyoteGameConfigService { return this.configCache.get(cacheKey); } + const configUpdater = this.configUpdaters[type]; + if (!configUpdater) { + throw new Error(`Config type not found: ${type}`); + } + const configPath = path.join(this.gameConfigDir, `${clientId}.${type}.json`); if (fs.existsSync(configPath)) { const fileContent = await fs.promises.readFile(configPath, { encoding: 'utf-8' }); const config = JSON.parse(fileContent); - this.configCache.set(cacheKey, config); - return config; + + let updatedConfig = config; + // 在获取配置时,如果版本不一致,则更新配置schema + if (config.version !== configUpdater.getCurrentVersion()) { + updatedConfig = configUpdater.updateObject(config); + await fs.promises.writeFile(configPath, JSON.stringify(updatedConfig, null, 4), { encoding: 'utf-8' }); + } + + this.configCache.set(cacheKey, updatedConfig); + return updatedConfig; } if (useDefault) { - const defaultConfig = this.getDefaultConfig(type); + const defaultConfig = configUpdater.getDefaultEmptyObject(); this.configCache.set(cacheKey, defaultConfig); - return defaultConfig as any; + return defaultConfig; } return undefined; @@ -157,10 +161,10 @@ export class CoyoteGameConfigService { * @param toClientId */ public async copyAllConfigs(fromClientId: string, toClientId: string) { - for (const type of CoyoteGameConfigList) { - const config = await this.get(fromClientId, type, false); + for (const type of Object.keys(this.configUpdaters)) { + const config = await this.get(fromClientId, type as keyof GameConfigTypeMap, false); if (config) { - await this.set(toClientId, type, config); + await this.set(toClientId, type as keyof GameConfigTypeMap, config); } } } diff --git a/server/src/utils/MultipleLinkedMap.ts b/server/src/utils/MultipleLinkedMap.ts new file mode 100644 index 0000000..64a175a --- /dev/null +++ b/server/src/utils/MultipleLinkedMap.ts @@ -0,0 +1,98 @@ +import { simpleArrayDiff } from "./utils"; + +export class MultipleLinkedMap { + private _map = new Map(); + private _reverseMap = new Map(); + + public get map() { + return this._map; + } + + public get reverseMap() { + return this._reverseMap; + } + + public get keysCount() { + return this._map.size; + } + + public get valuesCount() { + return this._reverseMap.size; + } + + public keys() { + return this._map.keys(); + } + + public values() { + return this._reverseMap.keys(); + } + + public getFieldValues(key: K): V[] { + return this._map.get(key) || []; + } + + public getFieldKey(value: V): K | undefined { + return this._reverseMap.get(value); + } + + public addFieldValue(key: K, value: V) { + let values = this._map.get(key); + if (!values) { + values = []; + this._map.set(key, values); + } + values.push(value); + this._reverseMap.set(value, key); + } + + public removeFieldValue(key: K, value: V) { + let values = this._map.get(key); + if (values) { + let index = values.indexOf(value); + if (index !== -1) { + values.splice(index, 1); + if (values.length === 0) { + this._map.delete(key); + } + } + } + this._reverseMap.delete(value); + } + + public setFieldValues(key: K, values: V[]) { + let added = values; + let removed: V[] = []; + + let oldValues = this._map.get(key); + if (oldValues) { + let diffResult = simpleArrayDiff(oldValues, values); + added = diffResult.added; + removed = diffResult.removed; + } + + this._map.set(key, values); + + for (let value of removed) { + this._reverseMap.delete(value); + } + for (let value of added) { + this._reverseMap.set(value, key); + } + } + + public removeField(key: K) { + let values = this._map.get(key); + if (values) { + for (let value of values) { + this._reverseMap.delete(value); + } + } + this._map.delete(key); + } + + public clear() { + this._map.clear(); + this._reverseMap.clear(); + } +} \ No newline at end of file diff --git a/server/src/utils/ObjectUpdater.ts b/server/src/utils/ObjectUpdater.ts new file mode 100644 index 0000000..372924c --- /dev/null +++ b/server/src/utils/ObjectUpdater.ts @@ -0,0 +1,67 @@ +export interface ObjectSchemaStore { + version: number; + defaultEmptyObject: any; + upgrade: (oldObject: any) => any; +} + +export type VersionedObject = T & { version: number }; + +export class ObjectUpdater { + public objectSchemas: ObjectSchemaStore[] = []; + + constructor() { + this.registerSchemas(); + } + + /** + * 注册schema + * 继承的类可以重写这个方法,用于注册不同版本的schema + */ + protected registerSchemas(): void { + + } + + public addSchema(version: number, upgrade: (oldObject: any) => any, defaultEmptyObject: any = {}): void { + let prevSchema = this.objectSchemas[this.objectSchemas.length - 1]; + this.objectSchemas.push({ version, defaultEmptyObject, upgrade }); + + if (prevSchema.version > version) { // 如果添加的版本号比前一个版本号小,则重新排序 + this.objectSchemas.sort((a, b) => a.version - b.version); + } + } + + /** + * 将对象升级到最新版本 + * @param object + * @param version + * @returns + */ + public updateObject(object: T): T { + let currentObject = object; + let version = (object as any).version || 0; + for (let i = 0; i < this.objectSchemas.length; i++) { + let schema = this.objectSchemas[i]; + if (schema.version > version) { + currentObject = schema.upgrade(currentObject); + } + } + return currentObject; + } + + /** + * 获取默认的空对象 + * @returns + */ + public getDefaultEmptyObject(): any { + const defaultEmptyObject = this.objectSchemas[this.objectSchemas.length - 1].defaultEmptyObject; + if (typeof defaultEmptyObject === 'function') { + return defaultEmptyObject(); + } else { + return defaultEmptyObject; + } + } + + public getCurrentVersion(): number { + return this.objectSchemas[this.objectSchemas.length - 1].version; + } +} \ No newline at end of file diff --git a/server/src/utils/utils.ts b/server/src/utils/utils.ts index 6961097..5c58eb9 100644 --- a/server/src/utils/utils.ts +++ b/server/src/utils/utils.ts @@ -73,6 +73,17 @@ export const simpleObjDiff = (obj1: any, obj2: any) => { return false; } } + +export function simpleArrayDiff(arr1: T[], arr2: T[]): { added: T[], removed: T[] } { + const set1 = new Set(arr1); + const set2 = new Set(arr2); + + const added = arr2.filter(item => !set1.has(item)); + const removed = arr1.filter(item => !set2.has(item)); + + return { added, removed }; +} + export class LocalIPAddress { private static ipAddrList?: string[];