Commit 83e457e0 authored by nanahira's avatar nanahira

tournament mode

parent 41a6b406
......@@ -35,10 +35,14 @@
- srvpro 里面的 client.send(发送给客户端)还是对应 client.send
- srvpro 里面 server.send(模拟客户端发送消息)对应 this.ctx.dispatch(msgClassInstance, client)
## ts-rest 相关
## ts-rest 相关(如果用到的话)
- 契约文件放在 src/api/contract.ts 一个文件里面,方便其他项目复制。
## TypeORM 相关
- 删除一律用 softDelete。
## 参考项目
可以参考电脑的下面的项目,用来参考。这些代码只能看,不能改。~ 指代这台电脑的 HOME 目录。
......
......@@ -62,6 +62,9 @@ chatgptTokenLimit: 12000
chatgptExtraOpts: {}
enableReconnect: 1
enableCloudReplay: 1
tournamentMode: 0
tournamentModeCheckDeck: 1
blockReplayToPlayer: 0
enableRoomlist: 1
reconnectTimeout: 180000
hidePlayerName: 0
......
......@@ -142,6 +142,15 @@ export const defaultConfig = {
// Enable cloud replay menu entry (R/W pass handling).
// Boolean parse rule (default true): only '0'/'false'/'null' => false, otherwise true.
ENABLE_CLOUD_REPLAY: '1',
// Enable tournament mode compatibility behavior.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
TOURNAMENT_MODE: '0',
// Enable tournament mode deck lock check hook.
// Boolean parse rule (default true): only '0'/'false'/'null' => false, otherwise true.
TOURNAMENT_MODE_CHECK_DECK: '1',
// Block replay packets to players who are currently in a room.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
BLOCK_REPLAY_TO_PLAYER: '0',
// Enable room list menu entry (L pass handling).
// Boolean parse rule (default true): only '0'/'false'/'null' => false, otherwise true.
ENABLE_ROOMLIST: '1',
......
......@@ -33,6 +33,14 @@ export const TRANSLATIONS = {
pre_reconnecting_to_room:
'You will be reconnected to your previous game. Please pick your previous deck.',
deck_incorrect_reconnect: 'Please pick your previous deck.',
deck_correct_part1: 'Your deck ',
deck_correct_part2: ' has passed the deck check for this tournament.',
deck_incorrect_part1:
'The deck you are using is not the same as your submitted deck ',
deck_incorrect_part2:
' . Please make sure all the cards are in the correct sequence.',
deck_not_found:
', we did not receive your registration. Please make sure you are using the correct ID for the tournament.',
reconnect_failed: 'Reconnect failed.',
reconnecting_to_room: 'Reconnecting to server...',
side_timeout_part1: 'Changing side time is limited to ',
......@@ -166,6 +174,11 @@ export const TRANSLATIONS = {
pre_reconnecting_to_room:
'你有未完成的对局,即将重新连接,请选择你在本局决斗中使用的卡组并准备。',
deck_incorrect_reconnect: '请选择你在本局决斗中使用的卡组。',
deck_correct_part1: '成功使用卡组 ',
deck_correct_part2: ' 参加比赛。',
deck_incorrect_part1: '您的卡组与报名卡组 ',
deck_incorrect_part2: ' 不符。注意卡组不能有包括卡片顺序在内的任何修改。',
deck_not_found: ',没有找到您的报名信息,请确定您使用昵称与报名ID一致。',
reconnect_failed: '重新连接失败。',
reconnecting_to_room: '正在重新连接到服务器……',
side_timeout_part1: '你现在有',
......
import { YGOProStocReplay } from 'ygopro-msg-encode';
import { Context } from '../app';
export class BlockReplay {
private enabled = this.ctx.config.getBoolean('BLOCK_REPLAY_TO_PLAYER');
constructor(private ctx: Context) {
if (!this.enabled) {
return;
}
this.ctx.middleware(YGOProStocReplay, async (_msg, client, next) => {
if (client.roomName) {
return;
}
return next();
});
}
}
import { Context } from '../../app';
import { Client } from '../../client';
export class ClientKeyProvider {
constructor(private ctx: Context) {}
// Keep this switch for future compatibility with srvpro identity policies.
isLooseIdentityRule = false;
get isLooseIdentityRule() {
return this.ctx.config.getBoolean('TOURNAMENT_MODE');
}
getClientKey(client: Client): string {
if (!this.isLooseIdentityRule && client.vpass) {
......
......@@ -16,6 +16,8 @@ import { CommandsService, KoishiContextService } from '../koishi';
import { ChatgptService } from './chatgpt-service';
import { CloudReplayService } from './cloud-replay';
import { LpLowHintService } from './lp-low-hint-service';
import { LockDeckService } from './lock-deck-service';
import { BlockReplay } from './block-replay';
export const FeatsModule = createAppContext<ContextState>()
.provide(ClientKeyProvider)
......@@ -27,8 +29,10 @@ export const FeatsModule = createAppContext<ContextState>()
.provide(ClientVersionCheck)
.provide(PlayerStatusNotify)
.provide(CloudReplayService) // persist duel records
.provide(BlockReplay) // block replay packets for in-room players
.provide(ChatgptService) // AI-room chat replies
.provide(LpLowHintService) // low LP hint in duel
.provide(LockDeckService) // srvpro-style tournament deck lock check
.provide(RefreshFieldService)
.provide(Reconnect)
.provide(WaitForPlayerProvider) // chat refresh
......
export * from './client-version-check';
export * from './client-key-provider';
export * from './chatgpt-service';
export * from './block-replay';
export * from './cloud-replay';
export * from './hide-player-name-provider';
export * from './lock-deck-check';
export * from './lock-deck-service';
export * from './menu-manager';
export * from './welcome';
export * from './random-duel';
......
import YGOProDeck from 'ygopro-deck-encode';
import { Client } from '../client';
import { Room } from '../room';
import { ValueContainer } from '../utility/value-container';
export class LockDeckExpectedDeckCheck extends ValueContainer<
YGOProDeck | null | undefined
> {
constructor(
public room: Room,
public client: Client,
public deck: YGOProDeck,
) {
super(undefined);
}
get expectedDeck() {
return this.value;
}
}
import { YGOProLFListError, YGOProLFListErrorReason } from 'ygopro-lflist-encode';
import { ChatColor } from 'ygopro-msg-encode';
import { Context } from '../app';
import { RoomCheckDeck } from '../room';
import { isSrvproTournamentDeckEqual } from '../utility/deck-compare';
import { LockDeckExpectedDeckCheck } from './lock-deck-check';
class SrvproDeckBadError extends YGOProLFListError {
constructor() {
super(YGOProLFListErrorReason.LFLIST, 0);
}
toPayload() {
// srvpro 的 deck_bad 发的是 ERROR_MSG code=0
return 0;
}
}
export class LockDeckService {
constructor(private ctx: Context) {
if (!this.ctx.config.getBoolean('TOURNAMENT_MODE_CHECK_DECK')) {
return;
}
this.ctx.middleware(RoomCheckDeck, async (msg, client, next) => {
const current = await next();
if (msg.value) {
return current;
}
const expectedDeckCheck = await this.ctx.dispatch(
new LockDeckExpectedDeckCheck(msg.room, msg.client, msg.deck),
client,
);
const expectedDeck = expectedDeckCheck?.expectedDeck;
if (expectedDeck === undefined) {
return current;
}
if (expectedDeck === null) {
await client.sendChat(`${client.name}#{deck_not_found}`, ChatColor.RED);
return msg.use(new SrvproDeckBadError());
}
const deckName = expectedDeck.name || '';
if (isSrvproTournamentDeckEqual(msg.deck, expectedDeck)) {
await client.sendChat(
`#{deck_correct_part1}${deckName}#{deck_correct_part2}`,
ChatColor.BABYBLUE,
);
return current;
}
await client.sendChat(
`#{deck_incorrect_part1}${deckName}#{deck_incorrect_part2}`,
ChatColor.RED,
);
return msg.use(new SrvproDeckBadError());
});
}
}
......@@ -7,6 +7,10 @@ export class BadwordPlayerInfoChecker {
private badwordProvider = this.ctx.get(() => BadwordProvider);
constructor(private ctx: Context) {
if (this.ctx.config.getBoolean('TOURNAMENT_MODE')) {
return;
}
if (!this.badwordProvider.enabled) {
return;
}
......
......@@ -17,5 +17,6 @@ export * from './room-event/on-room-select-tp';
export * from './room-event/on-room-siding-ready';
export * from './room-event/on-room-siding-start';
export * from './room-event/on-room-win';
export * from './room-event/room-check-deck';
export * from './default-hostinfo-provder';
export * from './default-hostinfo';
......@@ -18,3 +18,21 @@ export function isUpdateDeckPayloadEqual(
return buffer1.equals(buffer2);
}
/**
* srvpro 锦标赛模式的对比方式:
* 1. 用 UPDATE_DECK payload 形态归一化(main+extra 统一到 payload 语义)
* 2. 忽略顺序进行比较
*/
export function isSrvproTournamentDeckEqual(
deck1: YGOProDeck,
deck2: YGOProDeck,
): boolean {
const normalizedDeck1 = YGOProDeck.fromUpdateDeckPayload(
deck1.toUpdateDeckPayload(),
);
const normalizedDeck2 = YGOProDeck.fromUpdateDeckPayload(
deck2.toUpdateDeckPayload(),
);
return normalizedDeck1.isEqual(normalizedDeck2, { ignoreOrder: true });
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment