Commit 397a408a authored by nanahira's avatar nanahira

handle sendChat by koishi Element

parent 2b6ddbcc
import { import { filter, merge, Observable, of, Subject } from 'rxjs';
filter, import { map, share, take, takeUntil, tap } from 'rxjs/operators';
from, import { h } from 'koishi';
lastValueFrom,
merge,
Observable,
of,
Subject,
} from 'rxjs';
import {
concatMap,
defaultIfEmpty,
ignoreElements,
map,
share,
take,
takeUntil,
tap,
} from 'rxjs/operators';
import { Context } from '../app'; import { Context } from '../app';
import { import {
YGOProCtos, YGOProCtos,
...@@ -38,6 +22,14 @@ import { Chnroute } from './chnroute'; ...@@ -38,6 +22,14 @@ import { Chnroute } from './chnroute';
import YGOProDeck from 'ygopro-deck-encode'; import YGOProDeck from 'ygopro-deck-encode';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { ClientRoomField } from '../utility/decorators'; import { ClientRoomField } from '../utility/decorators';
import {
collectKoishiTextTokens,
KoishiElement,
KoishiFragment,
OnSendChatElement,
resolveColoredMessages,
splitColoredMessagesByLine,
} from '../utility';
export class Client { export class Client {
protected async _send(data: Buffer): Promise<void> { protected async _send(data: Buffer): Promise<void> {
...@@ -149,42 +141,47 @@ export class Client { ...@@ -149,42 +141,47 @@ export class Client {
}); });
} }
async sendChat(msg: string, type: number = ChatColor.BABYBLUE) { async sendChat(msg: KoishiFragment, type: number = ChatColor.BABYBLUE) {
if (this.isInternal) { if (this.isInternal) {
return; return;
} }
if (type <= NetPlayerType.OBSERVER) { const normalizedType = typeof type === 'number' ? type : ChatColor.BABYBLUE;
return this.send( const elements = h.normalize(msg) as KoishiElement[];
new YGOProStocChat().fromPartial({ const tokens = await collectKoishiTextTokens(elements, (element) =>
msg: msg, this.resolveSendChatElement(element, normalizedType),
player_type: type, );
}), const messages = splitColoredMessagesByLine(
resolveColoredMessages(tokens, normalizedType),
); );
if (!messages.length) {
return;
} }
const locale = this.getLocale();
const lines = type <= NetPlayerType.OBSERVER ? [msg] : msg.split(/\r?\n/);
const sendTasks: Promise<unknown>[] = [];
await lastValueFrom( const locale = this.getLocale();
from(lines).pipe( for (const message of messages) {
concatMap((rawLine) => this.resolveChatLine(rawLine, type, locale)), const line = await this.resolveChatLine(
tap((line: string) => { message.text,
const sendTask = this.send( message.color,
locale,
);
await this.send(
new YGOProStocChat().fromPartial({ new YGOProStocChat().fromPartial({
msg: line, msg: line,
player_type: type, player_type: message.color,
}), }),
); );
if (sendTask) {
sendTasks.push(sendTask);
} }
}), }
ignoreElements(),
defaultIfEmpty(undefined),
),
);
await Promise.all(sendTasks); private async resolveSendChatElement(element: KoishiElement, type: number) {
const event = await this.ctx.dispatch(
new OnSendChatElement(this, type, element),
this,
);
if (!event || event.value === undefined) {
return undefined;
}
return event.value;
} }
private async resolveChatLine(rawLine: string, type: number, locale: string) { private async resolveChatLine(rawLine: string, type: number, locale: string) {
......
import { NetPlayerType, YGOProStocHsPlayerEnter } from 'ygopro-msg-encode'; import { NetPlayerType, YGOProStocHsPlayerEnter } from 'ygopro-msg-encode';
import { h } from 'koishi';
import { Context } from '../app'; import { Context } from '../app';
import { DuelStage, OnRoomGameStart, Room, RoomManager } from '../room'; import { DuelStage, OnRoomGameStart, RoomManager } from '../room';
import { Client } from '../client'; import { Client } from '../client';
import { OnSendChatElement, PlayerNameClient } from '../utility';
declare module '../room' { declare module '../room' {
interface Room { interface Room {
...@@ -41,15 +43,33 @@ export class HidePlayerNameProvider { ...@@ -41,15 +43,33 @@ export class HidePlayerNameProvider {
return `Player ${client.pos + 1}`; return `Player ${client.pos + 1}`;
} }
getHidPlayerNameFactory(client: Pick<Client, 'pos' | 'name' | 'roomName'>) {
return (sightPlayer?: Client) => this.getHidPlayerName(client, sightPlayer);
}
async init() { async init() {
if (!this.enabled) { if (!this.enabled) {
return; return;
} }
this.ctx.middleware(OnSendChatElement, async (event, client, next) => {
const element = event.value;
if (!element || element.type !== 'PlayerName') {
return next();
}
const sourceClient = element.attrs?.client as
| PlayerNameClient
| undefined;
if (!sourceClient) {
return next();
}
const hidPlayerName = this.getHidPlayerName(sourceClient, client);
event.use(
h(
'PlayerName',
{ client: sourceClient },
hidPlayerName || sourceClient.name || '',
),
);
return next();
});
this.ctx.middleware(YGOProStocHsPlayerEnter, async (msg, client, next) => { this.ctx.middleware(YGOProStocHsPlayerEnter, async (msg, client, next) => {
const hidPlayerName = this.getHidPlayerName(msg, client); const hidPlayerName = this.getHidPlayerName(msg, client);
if (hidPlayerName !== msg.name) { if (hidPlayerName !== msg.name) {
......
...@@ -6,7 +6,7 @@ import { ChatColor } from 'ygopro-msg-encode'; ...@@ -6,7 +6,7 @@ import { ChatColor } from 'ygopro-msg-encode';
import { Context } from '../../app'; import { Context } from '../../app';
import { RoomCheckDeck } from '../../room'; import { RoomCheckDeck } from '../../room';
import { isUpdateDeckPayloadEqual } from '../../utility/deck-compare'; import { isUpdateDeckPayloadEqual } from '../../utility/deck-compare';
import { HidePlayerNameProvider } from '../hide-player-name-provider'; import { PlayerName } from '../../utility';
import { LockDeckExpectedDeckCheck } from './lock-deck-check'; import { LockDeckExpectedDeckCheck } from './lock-deck-check';
class SrvproDeckBadError extends YGOProLFListError { class SrvproDeckBadError extends YGOProLFListError {
...@@ -21,8 +21,6 @@ class SrvproDeckBadError extends YGOProLFListError { ...@@ -21,8 +21,6 @@ class SrvproDeckBadError extends YGOProLFListError {
} }
export class LockDeckService { export class LockDeckService {
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
constructor(private ctx: Context) {} constructor(private ctx: Context) {}
async init() { async init() {
...@@ -47,11 +45,10 @@ export class LockDeckService { ...@@ -47,11 +45,10 @@ export class LockDeckService {
} }
if (expectedDeck === null) { if (expectedDeck === null) {
const playerName = this.hidePlayerNameProvider.getHidPlayerName( await client.sendChat(
client, [PlayerName(client), '#{deck_not_found}'],
client, ChatColor.RED,
); );
await client.sendChat(`${playerName}#{deck_not_found}`, ChatColor.RED);
return msg.use(new SrvproDeckBadError()); return msg.use(new SrvproDeckBadError());
} }
......
import { ChatColor, NetPlayerType } from 'ygopro-msg-encode'; import { ChatColor, NetPlayerType } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../app';
import { HidePlayerNameProvider } from './hide-player-name-provider';
import { OnRoomJoinObserver } from '../room/room-event/on-room-join-observer'; import { OnRoomJoinObserver } from '../room/room-event/on-room-join-observer';
import { OnRoomLeave } from '../room/room-event/on-room-leave'; import { OnRoomLeave } from '../room/room-event/on-room-leave';
import { PlayerName } from '../utility';
export class PlayerStatusNotify { export class PlayerStatusNotify {
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
constructor(private ctx: Context) {} constructor(private ctx: Context) {}
async init() { async init() {
...@@ -14,8 +12,7 @@ export class PlayerStatusNotify { ...@@ -14,8 +12,7 @@ export class PlayerStatusNotify {
this.ctx.middleware(OnRoomJoinObserver, async (event, client, next) => { this.ctx.middleware(OnRoomJoinObserver, async (event, client, next) => {
const room = event.room; const room = event.room;
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{watch_join}'],
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{watch_join}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
return next(); return next();
...@@ -27,15 +24,13 @@ export class PlayerStatusNotify { ...@@ -27,15 +24,13 @@ export class PlayerStatusNotify {
if (client.pos === NetPlayerType.OBSERVER) { if (client.pos === NetPlayerType.OBSERVER) {
// 观战者离开 // 观战者离开
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{quit_watch}'],
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{quit_watch}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
} else { } else {
// 玩家离开 // 玩家离开
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{left_game}'],
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{left_game}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
} }
......
...@@ -27,6 +27,7 @@ import { CanReconnectCheck } from '../reconnect'; ...@@ -27,6 +27,7 @@ import { CanReconnectCheck } from '../reconnect';
import { WaitForPlayerProvider } from '../wait-for-player-provider'; import { WaitForPlayerProvider } from '../wait-for-player-provider';
import { ClientKeyProvider } from '../client-key-provider'; import { ClientKeyProvider } from '../client-key-provider';
import { HidePlayerNameProvider } from '../hide-player-name-provider'; import { HidePlayerNameProvider } from '../hide-player-name-provider';
import { KoishiElement, PlayerName } from '../../utility';
import { RandomDuelScore } from './score.entity'; import { RandomDuelScore } from './score.entity';
import { import {
formatRemainText, formatRemainText,
...@@ -531,8 +532,7 @@ export class RandomDuelProvider { ...@@ -531,8 +532,7 @@ export class RandomDuelProvider {
} }
if (abuseCount >= 5) { if (abuseCount >= 5) {
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{chat_banned}'],
`${this.hidePlayerName.getHidPlayerName(client, sightPlayer)} #{chat_banned}`,
ChatColor.RED, ChatColor.RED,
); );
await this.punishPlayer(client, 'ABUSE'); await this.punishPlayer(client, 'ABUSE');
...@@ -751,11 +751,10 @@ export class RandomDuelProvider { ...@@ -751,11 +751,10 @@ export class RandomDuelProvider {
const clientScoreText = await this.getScoreDisplay( const clientScoreText = await this.getScoreDisplay(
this.getClientKey(client), this.getClientKey(client),
); );
const nameFactory = this.hidePlayerName.getHidPlayerNameFactory(client);
for (const player of players) { for (const player of players) {
if (clientScoreText) { if (clientScoreText) {
await player.sendChat( await player.sendChat(
clientScoreText(nameFactory(player)), clientScoreText(PlayerName(client)),
ChatColor.GREEN, ChatColor.GREEN,
); );
} }
...@@ -767,7 +766,7 @@ export class RandomDuelProvider { ...@@ -767,7 +766,7 @@ export class RandomDuelProvider {
); );
if (playerScoreText) { if (playerScoreText) {
await client.sendChat( await client.sendChat(
playerScoreText(this.hidePlayerName.getHidPlayerName(player, client)), playerScoreText(PlayerName(player)),
ChatColor.GREEN, ChatColor.GREEN,
); );
} }
...@@ -780,14 +779,14 @@ export class RandomDuelProvider { ...@@ -780,14 +779,14 @@ export class RandomDuelProvider {
return undefined; return undefined;
} }
const score = await repo.findOneBy({ name }); const score = await repo.findOneBy({ name });
return (displayName: string) => { return (displayName: string | KoishiElement) => {
if (!score) { if (!score) {
return `${displayName} #{random_score_blank}`; return [displayName, ' #{random_score_blank}'];
} }
const total = score.winCount + score.loseCount; const total = score.winCount + score.loseCount;
if (score.winCount < 2 && total < 3) { if (score.winCount < 2 && total < 3) {
return `${displayName} #{random_score_not_enough}`; return [displayName, ' #{random_score_not_enough}'];
} }
const safeTotal = total > 0 ? total : 1; const safeTotal = total > 0 ? total : 1;
...@@ -795,9 +794,17 @@ export class RandomDuelProvider { ...@@ -795,9 +794,17 @@ export class RandomDuelProvider {
const fleeRate = Math.ceil((score.fleeCount / safeTotal) * 100); const fleeRate = Math.ceil((score.fleeCount / safeTotal) * 100);
if (score.winCombo >= 2) { if (score.winCombo >= 2) {
return `#{random_score_part1}${displayName} #{random_score_part2} ${winRate}#{random_score_part3} ${fleeRate}#{random_score_part4_combo}${score.winCombo}#{random_score_part5_combo}`; return [
} '#{random_score_part1}',
return `#{random_score_part1}${displayName} #{random_score_part2} ${winRate}#{random_score_part3} ${fleeRate}#{random_score_part4}`; displayName,
` #{random_score_part2} ${winRate}#{random_score_part3} ${fleeRate}#{random_score_part4_combo}${score.winCombo}#{random_score_part5_combo}`,
];
}
return [
'#{random_score_part1}',
displayName,
` #{random_score_part2} ${winRate}#{random_score_part3} ${fleeRate}#{random_score_part4}`,
];
}; };
} }
......
...@@ -22,9 +22,9 @@ import { DuelStage, Room, RoomManager } from '../../room'; ...@@ -22,9 +22,9 @@ import { DuelStage, Room, RoomManager } from '../../room';
import { getSpecificFields } from '../../utility/metadata'; import { getSpecificFields } from '../../utility/metadata';
import { YGOProCtosDisconnect } from '../../utility/ygopro-ctos-disconnect'; import { YGOProCtosDisconnect } from '../../utility/ygopro-ctos-disconnect';
import { isUpdateDeckPayloadEqual } from '../../utility/deck-compare'; import { isUpdateDeckPayloadEqual } from '../../utility/deck-compare';
import { PlayerName } from '../../utility';
import { CanReconnectCheck } from './can-reconnect-check'; import { CanReconnectCheck } from './can-reconnect-check';
import { ClientKeyProvider } from '../client-key-provider'; import { ClientKeyProvider } from '../client-key-provider';
import { HidePlayerNameProvider } from '../hide-player-name-provider';
import { RefreshFieldService } from './refresh-field-service'; import { RefreshFieldService } from './refresh-field-service';
interface DisconnectInfo { interface DisconnectInfo {
...@@ -58,7 +58,6 @@ export class Reconnect { ...@@ -58,7 +58,6 @@ export class Reconnect {
private disconnectList = new Map<string, DisconnectInfo>(); private disconnectList = new Map<string, DisconnectInfo>();
private reconnectTimeout = this.ctx.config.getInt('RECONNECT_TIMEOUT'); // 超时时间,单位:毫秒(默认 180000ms = 3分钟) private reconnectTimeout = this.ctx.config.getInt('RECONNECT_TIMEOUT'); // 超时时间,单位:毫秒(默认 180000ms = 3分钟)
private clientKeyProvider = this.ctx.get(() => ClientKeyProvider); private clientKeyProvider = this.ctx.get(() => ClientKeyProvider);
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
private refreshFieldService = this.ctx.get(() => RefreshFieldService); private refreshFieldService = this.ctx.get(() => RefreshFieldService);
constructor(private ctx: Context) {} constructor(private ctx: Context) {}
...@@ -147,8 +146,7 @@ export class Reconnect { ...@@ -147,8 +146,7 @@ export class Reconnect {
// 通知房间 // 通知房间
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{disconnect_from_game}'],
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{disconnect_from_game}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
...@@ -286,8 +284,7 @@ export class Reconnect { ...@@ -286,8 +284,7 @@ export class Reconnect {
// 通知房间 // 通知房间
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{reconnect_to_game}'],
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{reconnect_to_game}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
...@@ -312,8 +309,7 @@ export class Reconnect { ...@@ -312,8 +309,7 @@ export class Reconnect {
// 通知房间 // 通知房间
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{reconnect_to_game}'],
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{reconnect_to_game}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
...@@ -328,8 +324,6 @@ export class Reconnect { ...@@ -328,8 +324,6 @@ export class Reconnect {
} }
} }
private hidePlayerName = this.ctx.get(() => HidePlayerNameProvider);
private async sendPreReconnectInfo( private async sendPreReconnectInfo(
client: Client, client: Client,
room: Room, room: Room,
...@@ -363,7 +357,7 @@ export class Reconnect { ...@@ -363,7 +357,7 @@ export class Reconnect {
if (player) { if (player) {
await client.send( await client.send(
new YGOProStocHsPlayerEnter().fromPartial({ new YGOProStocHsPlayerEnter().fromPartial({
name: this.hidePlayerNameProvider.getHidPlayerName(player, client), name: player.name,
pos: player.pos, pos: player.pos,
}), }),
); );
......
import { ChatColor, YGOProMsgNewTurn } from 'ygopro-msg-encode'; import { ChatColor, YGOProMsgNewTurn } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../app';
import { RoomManager, DuelStage, OnRoomDuelStart, Room } from '../room'; import { RoomManager, DuelStage, OnRoomDuelStart, Room } from '../room';
import { HidePlayerNameProvider } from './hide-player-name-provider'; import { PlayerName } from '../utility';
const DEATH_WIN_REASON = 0x11; const DEATH_WIN_REASON = 0x11;
...@@ -13,7 +13,6 @@ declare module '../room' { ...@@ -13,7 +13,6 @@ declare module '../room' {
export class RoomDeathService { export class RoomDeathService {
private roomManager = this.ctx.get(() => RoomManager); private roomManager = this.ctx.get(() => RoomManager);
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
constructor(private ctx: Context) {} constructor(private ctx: Context) {}
...@@ -57,18 +56,14 @@ export class RoomDeathService { ...@@ -57,18 +56,14 @@ export class RoomDeathService {
if (lp0 !== lp1 && room.turnCount > 1) { if (lp0 !== lp1 && room.turnCount > 1) {
const winner = lp0 > lp1 ? 0 : 1; const winner = lp0 > lp1 ? 0 : 1;
const winnerPlayer = room.getDuelPosPlayers(winner)[0]; const winnerPlayer = room.getDuelPosPlayers(winner)[0];
await room.sendChat( const finishMessage = winnerPlayer
(sightPlayer) => ? [
`#{death_finish_part1}${ '#{death_finish_part1}',
winnerPlayer PlayerName(winnerPlayer),
? this.hidePlayerNameProvider.getHidPlayerName( '#{death_finish_part2}',
winnerPlayer, ]
sightPlayer, : '#{death_finish_part1}#{death_finish_part2}';
) await room.sendChat(finishMessage, ChatColor.BABYBLUE);
: ''
}#{death_finish_part2}`,
ChatColor.BABYBLUE,
);
await room.win({ await room.win({
player: room.getIngameDuelPosByDuelPos(winner), player: room.getIngameDuelPosByDuelPos(winner),
type: DEATH_WIN_REASON, type: DEATH_WIN_REASON,
...@@ -107,18 +102,14 @@ export class RoomDeathService { ...@@ -107,18 +102,14 @@ export class RoomDeathService {
) { ) {
const winner = score0 > score1 ? 0 : 1; const winner = score0 > score1 ? 0 : 1;
const winnerPlayer = room.getDuelPosPlayers(winner)[0]; const winnerPlayer = room.getDuelPosPlayers(winner)[0];
await room.sendChat( const finishMessage = winnerPlayer
(sightPlayer) => ? [
`#{death2_finish_part1}${ '#{death2_finish_part1}',
winnerPlayer PlayerName(winnerPlayer),
? this.hidePlayerNameProvider.getHidPlayerName( '#{death2_finish_part2}',
winnerPlayer, ]
sightPlayer, : '#{death2_finish_part1}#{death2_finish_part2}';
) await room.sendChat(finishMessage, ChatColor.BABYBLUE);
: ''
}#{death2_finish_part2}`,
ChatColor.BABYBLUE,
);
await room.win( await room.win(
{ {
player: room.getIngameDuelPosByDuelPos(winner), player: room.getIngameDuelPosByDuelPos(winner),
......
...@@ -9,9 +9,9 @@ import { ...@@ -9,9 +9,9 @@ import {
OnRoomSidingStart, OnRoomSidingStart,
Room, Room,
} from '../room'; } from '../room';
import { HidePlayerNameProvider } from './hide-player-name-provider';
import { merge, Subscription, timer } from 'rxjs'; import { merge, Subscription, timer } from 'rxjs';
import { filter, finalize, share, take, takeUntil } from 'rxjs/operators'; import { filter, finalize, share, take, takeUntil } from 'rxjs/operators';
import { PlayerName } from '../utility';
declare module '../room' { declare module '../room' {
interface Room { interface Room {
...@@ -23,7 +23,6 @@ declare module '../room' { ...@@ -23,7 +23,6 @@ declare module '../room' {
export class SideTimeout { export class SideTimeout {
private logger = this.ctx.createLogger('SideTimeout'); private logger = this.ctx.createLogger('SideTimeout');
private sideTimeoutMinutes = this.ctx.config.getInt('SIDE_TIMEOUT_MINUTES'); private sideTimeoutMinutes = this.ctx.config.getInt('SIDE_TIMEOUT_MINUTES');
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
private onSidingReady$ = this.ctx.event$(OnRoomSidingReady).pipe(share()); private onSidingReady$ = this.ctx.event$(OnRoomSidingReady).pipe(share());
private onLeavePlayer$ = this.ctx.event$(OnRoomLeavePlayer).pipe(share()); private onLeavePlayer$ = this.ctx.event$(OnRoomLeavePlayer).pipe(share());
private onGameStart$ = this.ctx.event$(OnRoomGameStart).pipe(share()); private onGameStart$ = this.ctx.event$(OnRoomGameStart).pipe(share());
...@@ -150,8 +149,7 @@ export class SideTimeout { ...@@ -150,8 +149,7 @@ export class SideTimeout {
if (remainMinutes <= 1) { if (remainMinutes <= 1) {
this.clearSideTimeout(room, pos); this.clearSideTimeout(room, pos);
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(client), ' #{side_overtime_room}'],
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{side_overtime_room}`,
ChatColor.BABYBLUE, ChatColor.BABYBLUE,
); );
await client.sendChat('#{side_overtime}', ChatColor.RED); await client.sendChat('#{side_overtime}', ChatColor.RED);
......
...@@ -20,8 +20,8 @@ import { ...@@ -20,8 +20,8 @@ import {
Room, Room,
RoomManager, RoomManager,
} from '../room'; } from '../room';
import { HidePlayerNameProvider } from './hide-player-name-provider';
import { OnClientWaitTimeout } from './random-duel/random-duel-events'; import { OnClientWaitTimeout } from './random-duel/random-duel-events';
import { PlayerName } from '../utility';
export interface WaitForPlayerConfig { export interface WaitForPlayerConfig {
roomFilter: (room: Room) => boolean; roomFilter: (room: Room) => boolean;
...@@ -53,7 +53,6 @@ interface WaitForPlayerTickRuntime { ...@@ -53,7 +53,6 @@ interface WaitForPlayerTickRuntime {
export class WaitForPlayerProvider { export class WaitForPlayerProvider {
private logger = this.ctx.createLogger(this.constructor.name); private logger = this.ctx.createLogger(this.constructor.name);
private roomManager = this.ctx.get(() => RoomManager); private roomManager = this.ctx.get(() => RoomManager);
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
private tickRuntimes = new Map<number, WaitForPlayerTickRuntime>(); private tickRuntimes = new Map<number, WaitForPlayerTickRuntime>();
private nextTickId = 1; private nextTickId = 1;
...@@ -394,8 +393,7 @@ export class WaitForPlayerProvider { ...@@ -394,8 +393,7 @@ export class WaitForPlayerProvider {
) { ) {
room.waitForPlayerReadyWarnRemain = remainSeconds; room.waitForPlayerReadyWarnRemain = remainSeconds;
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(target), ` ${remainSeconds} #{kick_count_down}`],
`${this.hidePlayerNameProvider.getHidPlayerName(target, sightPlayer)} ${remainSeconds} #{kick_count_down}`,
remainSeconds <= 9 ? ChatColor.RED : ChatColor.LIGHTBLUE, remainSeconds <= 9 ? ChatColor.RED : ChatColor.LIGHTBLUE,
); );
} }
...@@ -414,8 +412,7 @@ export class WaitForPlayerProvider { ...@@ -414,8 +412,7 @@ export class WaitForPlayerProvider {
return; return;
} }
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(latestTarget), ' #{kicked_by_system}'],
`${this.hidePlayerNameProvider.getHidPlayerName(latestTarget, sightPlayer)} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await this.ctx.dispatch( await this.ctx.dispatch(
...@@ -466,8 +463,7 @@ export class WaitForPlayerProvider { ...@@ -466,8 +463,7 @@ export class WaitForPlayerProvider {
room.lastActiveTime = new Date(nowMs); room.lastActiveTime = new Date(nowMs);
room.waitForPlayerHangWarnElapsed = undefined; room.waitForPlayerHangWarnElapsed = undefined;
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(waitingPlayer), ' #{kicked_by_system}'],
`${this.hidePlayerNameProvider.getHidPlayerName(waitingPlayer, sightPlayer)} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await this.ctx.dispatch( await this.ctx.dispatch(
...@@ -490,8 +486,10 @@ export class WaitForPlayerProvider { ...@@ -490,8 +486,10 @@ export class WaitForPlayerProvider {
); );
if (remainSeconds > 0) { if (remainSeconds > 0) {
await room.sendChat( await room.sendChat(
(sightPlayer) => [
`${this.hidePlayerNameProvider.getHidPlayerName(waitingPlayer, sightPlayer)} #{afk_warn_part1}${remainSeconds}#{afk_warn_part2}`, PlayerName(waitingPlayer),
` #{afk_warn_part1}${remainSeconds}#{afk_warn_part2}`,
],
ChatColor.RED, ChatColor.RED,
); );
} }
......
import { import {
Bot, Bot,
Context as KoishiContext, Context as KoishiContext,
Fragment as KoishiFragment,
Session as KoishiSession, Session as KoishiSession,
Universal, Universal,
h,
} from 'koishi'; } from 'koishi';
import * as koishiHelpModule from '@koishijs/plugin-help'; import * as koishiHelpModule from '@koishijs/plugin-help';
import { ChatColor, YGOProCtosChat } from 'ygopro-msg-encode'; import { YGOProCtosChat } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../app';
import { Client } from '../client'; import { Client } from '../client';
import { ClientKeyProvider } from '../feats'; import { ClientKeyProvider } from '../feats';
import { I18nService } from '../client/i18n'; import { I18nService } from '../client/i18n';
import { Room, RoomManager } from '../room'; import { Room, RoomManager } from '../room';
type KoishiElement = h; import { KoishiFragment } from '../utility';
const koishiHelp = const koishiHelp =
(koishiHelpModule as any).default || (koishiHelpModule as any); (koishiHelpModule as any).default || (koishiHelpModule as any);
...@@ -22,16 +20,6 @@ type KoishiReferrer = { ...@@ -22,16 +20,6 @@ type KoishiReferrer = {
userId: string; userId: string;
}; };
type ChatToken = {
text: string;
color?: number;
};
type ColoredChatMessage = {
text: string;
color: number;
};
type CommandContext = { type CommandContext = {
room: Room; room: Room;
client: Client; client: Client;
...@@ -310,22 +298,8 @@ export class KoishiContextService { ...@@ -310,22 +298,8 @@ export class KoishiContextService {
return []; return [];
} }
const messages = this.resolveColoredMessages(h.normalize(content)); await Promise.all(targets.map((target) => target.sendChat(content)));
if (!messages.length) { return [`${Date.now()}-${Math.random().toString(36).slice(2, 8)}`];
return [];
}
const messageIds: string[] = [];
for (let i = 0; i < messages.length; i += 1) {
const message = messages[i];
await Promise.all(
targets.map((target) => target.sendChat(message.text, message.color)),
);
messageIds.push(
`${Date.now()}-${i}-${Math.random().toString(36).slice(2, 8)}`,
);
}
return messageIds;
} }
private resolveSendTargets(room: Room, session?: KoishiSession) { private resolveSendTargets(room: Room, session?: KoishiSession) {
...@@ -336,139 +310,4 @@ export class KoishiContextService { ...@@ -336,139 +310,4 @@ export class KoishiContextService {
const target = this.findClientByUserId(room, referrer.userId); const target = this.findClientByUserId(room, referrer.userId);
return target ? [target] : []; return target ? [target] : [];
} }
private resolveColoredMessages(
elements: KoishiElement[],
): ColoredChatMessage[] {
const tokens = this.collectTextTokens(elements);
if (!tokens.length) {
return [];
}
const cleanedTokens = tokens.filter((token) => token.text.length > 0);
if (!cleanedTokens.length) {
return [];
}
const firstColoredIndex = cleanedTokens.findIndex(
(token) => typeof token.color === 'number',
);
if (firstColoredIndex === -1) {
return [
{
text: cleanedTokens.map((token) => token.text).join(''),
color: ChatColor.BABYBLUE,
},
];
}
let currentColor = cleanedTokens[firstColoredIndex].color as number;
let currentText = cleanedTokens
.slice(0, firstColoredIndex + 1)
.map((token) => token.text)
.join('');
const result: ColoredChatMessage[] = [];
for (let i = firstColoredIndex + 1; i < cleanedTokens.length; i += 1) {
const token = cleanedTokens[i];
if (typeof token.color === 'number' && token.color !== currentColor) {
if (currentText) {
result.push({
text: currentText,
color: currentColor,
});
}
currentColor = token.color;
currentText = token.text;
} else {
currentText += token.text;
}
}
if (currentText) {
result.push({
text: currentText,
color: currentColor,
});
}
if (!result.length) {
return [];
}
return result;
}
private collectTextTokens(
elements: KoishiElement[],
inheritedColor?: number,
): ChatToken[] {
const tokens: ChatToken[] = [];
for (const element of elements) {
if (!element) {
continue;
}
const color = this.resolveElementColor(element) ?? inheritedColor;
if (element.type === 'text') {
const content = element.attrs?.content;
if (typeof content === 'string' && content.length > 0) {
tokens.push({
text: content,
color,
});
}
} else if (element.type === 'br') {
tokens.push({
text: '\n',
color,
});
}
if (element.children?.length) {
tokens.push(
...this.collectTextTokens(element.children as KoishiElement[], color),
);
}
}
return tokens;
}
private resolveElementColor(element: KoishiElement): number | undefined {
const isChatElement =
typeof element.type === 'string' && element.type.toLowerCase() === 'chat';
const rawColor = isChatElement
? element.attrs?.color
: element.attrs?.chatColor;
if (rawColor == null) {
return undefined;
}
if (typeof rawColor === 'number') {
return this.normalizeChatColor(rawColor);
}
if (typeof rawColor !== 'string') {
return undefined;
}
const normalized = rawColor.replace(/[^a-z0-9]/gi, '').toUpperCase();
if (!normalized) {
return undefined;
}
const enumValue = (ChatColor as any)[normalized];
if (typeof enumValue === 'number') {
return enumValue;
}
const parsed = Number(rawColor);
if (Number.isFinite(parsed)) {
return this.normalizeChatColor(parsed);
}
return undefined;
}
private normalizeChatColor(value: number): number {
if (typeof (ChatColor as any)[value] === 'string') {
return value;
}
return ChatColor.BABYBLUE;
}
} }
...@@ -2,12 +2,11 @@ import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode'; ...@@ -2,12 +2,11 @@ import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../app';
import { LegacyBanEntity } from './legacy-ban.entity'; import { LegacyBanEntity } from './legacy-ban.entity';
import { RoomManager } from '../room'; import { RoomManager } from '../room';
import { HidePlayerNameProvider } from '../feats';
import { LegacyApiService } from './legacy-api-service'; import { LegacyApiService } from './legacy-api-service';
import { PlayerName } from '../utility';
export class LegacyBanService { export class LegacyBanService {
private logger = this.ctx.createLogger('LegacyBanService'); private logger = this.ctx.createLogger('LegacyBanService');
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
constructor(private ctx: Context) { constructor(private ctx: Context) {
this.ctx this.ctx
...@@ -78,8 +77,7 @@ export class LegacyBanService { ...@@ -78,8 +77,7 @@ export class LegacyBanService {
} }
await room.sendChat( await room.sendChat(
(sightPlayer) => [PlayerName(player), ' #{kicked_by_system}'],
`${this.hidePlayerNameProvider.getHidPlayerName(player, sightPlayer)} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await room.kick(player); await room.kick(player);
......
...@@ -108,6 +108,7 @@ import { OnRoomSelectTp } from './room-event/on-room-select-tp'; ...@@ -108,6 +108,7 @@ import { OnRoomSelectTp } from './room-event/on-room-select-tp';
import { RoomCheckDeck } from './room-event/room-check-deck'; import { RoomCheckDeck } from './room-event/room-check-deck';
import cryptoRandomString from 'crypto-random-string'; import cryptoRandomString from 'crypto-random-string';
import { RoomCurrentFieldInfo, RoomInfo } from './room-info'; import { RoomCurrentFieldInfo, RoomInfo } from './room-info';
import { KoishiFragment } from '../utility';
const { OcgcoreScriptConstants } = _OcgcoreConstants; const { OcgcoreScriptConstants } = _OcgcoreConstants;
...@@ -959,19 +960,11 @@ export class Room { ...@@ -959,19 +960,11 @@ export class Room {
return this.sendChat(msg.msg, this.getIngamePos(client)); return this.sendChat(msg.msg, this.getIngamePos(client));
} }
async sendChat( async sendChat(msg: KoishiFragment, type: number = ChatColor.BABYBLUE) {
msg: string | ((p: Client) => Awaitable<string>),
type: number = ChatColor.BABYBLUE,
) {
if (this.finalizing) { if (this.finalizing) {
return; return;
} }
return Promise.all( return Promise.all(this.allPlayers.map((p) => p.sendChat(msg, type)));
this.allPlayers.map(async (p) => {
const resolvedMessage = typeof msg === 'function' ? await msg(p) : msg;
return p.sendChat(resolvedMessage, type);
}),
);
} }
firstgoPos?: number; firstgoPos?: number;
......
...@@ -2,3 +2,4 @@ export * from './panel-pagination'; ...@@ -2,3 +2,4 @@ export * from './panel-pagination';
export * from './base-time.entity'; export * from './base-time.entity';
export * from './bigint-transformer'; export * from './bigint-transformer';
export * from './decorators'; export * from './decorators';
export * from './koishi-chat';
import { Fragment as KoishiFragment, h } from 'koishi';
import { ChatColor } from 'ygopro-msg-encode';
import { ValueContainer } from './value-container';
import type { Client } from '../client';
export type KoishiElement = h;
export { KoishiFragment };
export type ChatToken = {
text: string;
color?: number;
};
export type ColoredChatMessage = {
text: string;
color: number;
};
export type PlayerNameClient = Pick<Client, 'pos' | 'name' | 'roomName'>;
export class OnSendChatElement extends ValueContainer<
KoishiElement | undefined
> {
constructor(
public sightPlayer: Client,
public type: number,
element: KoishiElement,
) {
super(element);
}
}
export function PlayerName(
client: PlayerNameClient,
content: KoishiFragment = client.name || '',
) {
return h('PlayerName', { client }, content);
}
export function normalizeChatColor(value: number) {
if (typeof (ChatColor as any)[value] === 'string') {
return value;
}
return ChatColor.BABYBLUE;
}
export function resolveElementChatColor(
element: KoishiElement,
): number | undefined {
const isChatElement =
typeof element.type === 'string' && element.type.toLowerCase() === 'chat';
const rawColor = isChatElement
? element.attrs?.color
: element.attrs?.chatColor;
if (rawColor == null) {
return undefined;
}
if (typeof rawColor === 'number') {
return normalizeChatColor(rawColor);
}
if (typeof rawColor !== 'string') {
return undefined;
}
const normalized = rawColor.replace(/[^a-z0-9]/gi, '').toUpperCase();
if (!normalized) {
return undefined;
}
const enumValue = (ChatColor as any)[normalized];
if (typeof enumValue === 'number') {
return enumValue;
}
const parsed = Number(rawColor);
if (Number.isFinite(parsed)) {
return normalizeChatColor(parsed);
}
return undefined;
}
export async function collectKoishiTextTokens(
elements: KoishiElement[],
resolveElement?: (
element: KoishiElement,
) => Promise<KoishiElement | undefined>,
inheritedColor?: number,
): Promise<ChatToken[]> {
const tokens: ChatToken[] = [];
for (const rawElement of elements) {
if (!rawElement) {
continue;
}
const element = resolveElement
? await resolveElement(rawElement)
: rawElement;
if (!element) {
continue;
}
const color = resolveElementChatColor(element) ?? inheritedColor;
if (element.type === 'text') {
const content = element.attrs?.content;
if (typeof content === 'string' && content.length > 0) {
tokens.push({
text: content,
color,
});
}
} else if (element.type === 'br') {
tokens.push({
text: '\n',
color,
});
}
if (element.children?.length) {
tokens.push(
...(await collectKoishiTextTokens(
element.children as KoishiElement[],
resolveElement,
color,
)),
);
}
}
return tokens;
}
export function resolveColoredMessages(
tokens: ChatToken[],
defaultColor = ChatColor.BABYBLUE,
): ColoredChatMessage[] {
const cleanedTokens = tokens.filter((token) => token.text.length > 0);
if (!cleanedTokens.length) {
return [];
}
let currentColor = normalizeChatColor(defaultColor);
let currentText = '';
const result: ColoredChatMessage[] = [];
for (const token of cleanedTokens) {
const tokenColor =
typeof token.color === 'number'
? normalizeChatColor(token.color)
: currentColor;
if (tokenColor !== currentColor && currentText) {
result.push({
text: currentText,
color: currentColor,
});
currentText = '';
}
currentColor = tokenColor;
currentText += token.text;
}
if (currentText) {
result.push({
text: currentText,
color: currentColor,
});
}
return result;
}
export function splitColoredMessagesByLine(messages: ColoredChatMessage[]) {
const result: ColoredChatMessage[] = [];
let previousEndedWithNewline = false;
for (const message of messages) {
const lines = message.text.split(/\r?\n/);
const endedWithNewline = /\r?\n$/.test(message.text);
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const isFirst = i === 0;
const isLast = i === lines.length - 1;
if (isLast && line.length === 0 && endedWithNewline) {
continue;
}
if (
isFirst &&
line.length === 0 &&
!previousEndedWithNewline &&
result.length > 0
) {
continue;
}
result.push({
text: line,
color: message.color,
});
}
previousEndedWithNewline = endedWithNewline;
}
return result;
}
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