Skip to content

Commit

Permalink
增加用户配置自动更新
Browse files Browse the repository at this point in the history
  • Loading branch information
hyperzlib committed Nov 24, 2024
1 parent 1af39ca commit 2f97b0c
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 44 deletions.
1 change: 1 addition & 0 deletions server/src/controllers/game/CoyoteGameController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface CoyoteGameEvents {
close: [];
strengthChanged: [strength: GameStrengthInfo];
strengthConfigUpdated: [config: GameStrengthConfig];
identifiersUpdated: [identifiers: string[]];
clientConnected: [];
clientDisconnected: [];
gameStarted: [];
Expand Down
22 changes: 19 additions & 3 deletions server/src/managers/CoyoteGameManager.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -13,6 +13,7 @@ export class CoyoteGameManager {
private static _instance: CoyoteGameManager;

private games: Map<string, CoyoteGameController>;
private gameIdentifiers: MultipleLinkedMap<string, string> = new MultipleLinkedMap();

private events = new ExEventEmitter<CoyoteGameManagerEvents>();

Expand Down Expand Up @@ -56,15 +57,30 @@ 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);

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) {
Expand Down
12 changes: 12 additions & 0 deletions server/src/model/config/CustomPulseConfigUpdater.ts
Original file line number Diff line number Diff line change
@@ -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;
});
}
}
12 changes: 12 additions & 0 deletions server/src/model/config/GamePlayConfigUpdater.ts
Original file line number Diff line number Diff line change
@@ -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;
});
}
}
12 changes: 12 additions & 0 deletions server/src/model/config/GamePlayUserConfigUpdater.ts
Original file line number Diff line number Diff line change
@@ -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;
});
}
}
18 changes: 18 additions & 0 deletions server/src/model/config/MainGameConfigUpdater.ts
Original file line number Diff line number Diff line change
@@ -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;
});
}
}
86 changes: 45 additions & 41 deletions server/src/services/CoyoteGameConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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<CoyoteLiveGameManagerEvents>();

public configUpdaters: Record<string, ObjectUpdater> = {
[GameConfigType.MainGame]: new MainGameConfigUpdater(),
[GameConfigType.CustomPulse]: new CustomPulseConfigUpdater(),
[GameConfigType.GamePlay]: new GamePlayConfigUpdater(),
[GameConfigType.GamePlayUserConfig]: new GamePlayUserConfigUpdater(),
};

private configCache: LRUCache<string, any> = new LRUCache({
max: 1000,
Expand All @@ -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<TKey extends keyof GameConfigTypeMap>(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);
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
98 changes: 98 additions & 0 deletions server/src/utils/MultipleLinkedMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { simpleArrayDiff } from "./utils";

export class MultipleLinkedMap<K, V> {
private _map = new Map<K, V[]>();
private _reverseMap = new Map<V, K>();

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();
}
}
Loading

0 comments on commit 2f97b0c

Please sign in to comment.