Commit 56894877 authored by nanahira's avatar nanahira

add getHidPlayerName

parent a3e235cc
...@@ -2,3 +2,5 @@ webpack.config.js ...@@ -2,3 +2,5 @@ webpack.config.js
dist/* dist/*
build/* build/*
*.js *.js
/ygopro
/windbot
import { NetPlayerType, YGOProStocHsPlayerEnter } from 'ygopro-msg-encode'; import { NetPlayerType, YGOProStocHsPlayerEnter } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../app';
import { DuelStage, OnRoomGameStart, RoomManager } from '../room'; import { DuelStage, OnRoomGameStart, Room, RoomManager } from '../room';
import { Client } from '../client';
declare module '../room' { declare module '../room' {
interface Room { interface Room {
...@@ -14,30 +15,42 @@ export class HidePlayerNameProvider { ...@@ -14,30 +15,42 @@ export class HidePlayerNameProvider {
constructor(private ctx: Context) {} constructor(private ctx: Context) {}
getHidPlayerName(
client: Pick<Client, 'pos' | 'name' | 'roomName'>,
sightPlayer?: Client,
) {
if (!sightPlayer?.roomName) {
return client.name;
}
const room = this.roomManager.findByName(
client.roomName || sightPlayer?.roomName,
);
if (!room?.hidePlayerNames || !this.shouldHide(room.duelStage)) {
return client.name;
}
if (
client.pos < 0 ||
client.pos >= NetPlayerType.OBSERVER ||
(sightPlayer && sightPlayer.pos === client.pos) ||
!client.name
) {
return client.name;
}
return `Player ${client.pos + 1}`;
}
async init() { async init() {
if (!this.enabled) { if (!this.enabled) {
return; return;
} }
this.ctx.middleware(YGOProStocHsPlayerEnter, async (msg, client, next) => { this.ctx.middleware(YGOProStocHsPlayerEnter, async (msg, client, next) => {
if (!client.roomName) { const hidPlayerName = this.getHidPlayerName(msg, client);
return next(); if (hidPlayerName !== msg.name) {
msg.name = hidPlayerName;
} }
const room = this.roomManager.findByName(client.roomName);
if (!room?.hidePlayerNames || !this.shouldHide(room.duelStage)) {
return next();
}
const pos = msg.pos ?? -1;
if (
pos < 0 ||
pos >= NetPlayerType.OBSERVER ||
pos === client.pos ||
!msg.name
) {
return next();
}
msg.name = `Player ${pos + 1}`;
return next(); return next();
}); });
......
...@@ -6,6 +6,7 @@ import { ChatColor } from 'ygopro-msg-encode'; ...@@ -6,6 +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 { LockDeckExpectedDeckCheck } from './lock-deck-check'; import { LockDeckExpectedDeckCheck } from './lock-deck-check';
class SrvproDeckBadError extends YGOProLFListError { class SrvproDeckBadError extends YGOProLFListError {
...@@ -20,6 +21,8 @@ class SrvproDeckBadError extends YGOProLFListError { ...@@ -20,6 +21,8 @@ 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() {
...@@ -44,7 +47,11 @@ export class LockDeckService { ...@@ -44,7 +47,11 @@ export class LockDeckService {
} }
if (expectedDeck === null) { if (expectedDeck === null) {
await client.sendChat(`${client.name}#{deck_not_found}`, ChatColor.RED); const playerName = this.hidePlayerNameProvider.getHidPlayerName(
client,
client,
);
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';
export class PlayerStatusNotify { export class PlayerStatusNotify {
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
constructor(private ctx: Context) {} constructor(private ctx: Context) {}
async init() { async init() {
// 观战者加入 // 观战者加入
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(`${client.name} #{watch_join}`, ChatColor.LIGHTBLUE); await room.sendChat(
(sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{watch_join}`,
ChatColor.LIGHTBLUE,
);
return next(); return next();
}); });
...@@ -20,12 +27,17 @@ export class PlayerStatusNotify { ...@@ -20,12 +27,17 @@ export class PlayerStatusNotify {
if (client.pos === NetPlayerType.OBSERVER) { if (client.pos === NetPlayerType.OBSERVER) {
// 观战者离开 // 观战者离开
await room.sendChat( await room.sendChat(
`${client.name} #{quit_watch}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{quit_watch}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
} else { } else {
// 玩家离开 // 玩家离开
await room.sendChat(`${client.name} #{left_game}`, ChatColor.LIGHTBLUE); await room.sendChat(
(sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{left_game}`,
ChatColor.LIGHTBLUE,
);
} }
return next(); return next();
}); });
......
...@@ -100,6 +100,7 @@ export class RandomDuelProvider { ...@@ -100,6 +100,7 @@ export class RandomDuelProvider {
private clientKeyProvider = this.ctx.get(() => ClientKeyProvider); private clientKeyProvider = this.ctx.get(() => ClientKeyProvider);
private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider); private hidePlayerNameProvider = this.ctx.get(() => HidePlayerNameProvider);
private defaultHostInfoProvider = this.ctx.get(() => DefaultHostInfoProvider); private defaultHostInfoProvider = this.ctx.get(() => DefaultHostInfoProvider);
private hidePlayerName = this.ctx.get(() => HidePlayerNameProvider);
enabled = this.ctx.config.getBoolean('ENABLE_RANDOM_DUEL'); enabled = this.ctx.config.getBoolean('ENABLE_RANDOM_DUEL');
noRematchCheck = this.ctx.config.getBoolean('RANDOM_DUEL_NO_REMATCH_CHECK'); noRematchCheck = this.ctx.config.getBoolean('RANDOM_DUEL_NO_REMATCH_CHECK');
...@@ -530,7 +531,11 @@ export class RandomDuelProvider { ...@@ -530,7 +531,11 @@ export class RandomDuelProvider {
await this.unwelcome(room, client); await this.unwelcome(room, client);
} }
if (abuseCount >= 5) { if (abuseCount >= 5) {
await room.sendChat(`${client.name} #{chat_banned}`, ChatColor.RED); await room.sendChat(
(sightPlayer) =>
`${this.hidePlayerName.getHidPlayerName(client, sightPlayer)} #{chat_banned}`,
ChatColor.RED,
);
await this.punishPlayer(client, 'ABUSE'); await this.punishPlayer(client, 'ABUSE');
client.disconnect(); client.disconnect();
} }
...@@ -746,7 +751,7 @@ export class RandomDuelProvider { ...@@ -746,7 +751,7 @@ export class RandomDuelProvider {
const clientScoreText = await this.getScoreDisplay( const clientScoreText = await this.getScoreDisplay(
this.getClientKey(client), this.getClientKey(client),
client.name, this.hidePlayerName.getHidPlayerName(client, client),
); );
for (const player of players) { for (const player of players) {
if (clientScoreText) { if (clientScoreText) {
...@@ -757,7 +762,7 @@ export class RandomDuelProvider { ...@@ -757,7 +762,7 @@ export class RandomDuelProvider {
} }
const playerScoreText = await this.getScoreDisplay( const playerScoreText = await this.getScoreDisplay(
this.getClientKey(player), this.getClientKey(player),
player.name, this.hidePlayerName.getHidPlayerName(player, client),
); );
if (playerScoreText) { if (playerScoreText) {
await client.sendChat(playerScoreText, ChatColor.GREEN); await client.sendChat(playerScoreText, ChatColor.GREEN);
......
...@@ -24,6 +24,7 @@ import { YGOProCtosDisconnect } from '../../utility/ygopro-ctos-disconnect'; ...@@ -24,6 +24,7 @@ import { YGOProCtosDisconnect } from '../../utility/ygopro-ctos-disconnect';
import { isUpdateDeckPayloadEqual } from '../../utility/deck-compare'; import { isUpdateDeckPayloadEqual } from '../../utility/deck-compare';
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 {
...@@ -57,6 +58,7 @@ export class Reconnect { ...@@ -57,6 +58,7 @@ 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) {}
...@@ -145,7 +147,8 @@ export class Reconnect { ...@@ -145,7 +147,8 @@ export class Reconnect {
// 通知房间 // 通知房间
await room.sendChat( await room.sendChat(
`${client.name} #{disconnect_from_game}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{disconnect_from_game}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
...@@ -283,7 +286,8 @@ export class Reconnect { ...@@ -283,7 +286,8 @@ export class Reconnect {
// 通知房间 // 通知房间
await room.sendChat( await room.sendChat(
`${client.name} #{reconnect_to_game}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{reconnect_to_game}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
...@@ -308,7 +312,8 @@ export class Reconnect { ...@@ -308,7 +312,8 @@ export class Reconnect {
// 通知房间 // 通知房间
await room.sendChat( await room.sendChat(
`${client.name} #{reconnect_to_game}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(client, sightPlayer)} #{reconnect_to_game}`,
ChatColor.LIGHTBLUE, ChatColor.LIGHTBLUE,
); );
......
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';
const DEATH_WIN_REASON = 0x11; const DEATH_WIN_REASON = 0x11;
...@@ -12,6 +13,7 @@ declare module '../room' { ...@@ -12,6 +13,7 @@ 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) {}
...@@ -54,9 +56,17 @@ export class RoomDeathService { ...@@ -54,9 +56,17 @@ export class RoomDeathService {
if (room.turnCount >= room.death) { if (room.turnCount >= room.death) {
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 winnerName = room.getDuelPosPlayers(winner)[0]?.name || ''; const winnerPlayer = room.getDuelPosPlayers(winner)[0];
await room.sendChat( await room.sendChat(
`#{death_finish_part1}${winnerName}#{death_finish_part2}`, (sightPlayer) =>
`#{death_finish_part1}${
winnerPlayer
? this.hidePlayerNameProvider.getHidPlayerName(
winnerPlayer,
sightPlayer,
)
: ''
}#{death_finish_part2}`,
ChatColor.BABYBLUE, ChatColor.BABYBLUE,
); );
await room.win({ await room.win({
...@@ -96,9 +106,17 @@ export class RoomDeathService { ...@@ -96,9 +106,17 @@ export class RoomDeathService {
score0 !== score1 score0 !== score1
) { ) {
const winner = score0 > score1 ? 0 : 1; const winner = score0 > score1 ? 0 : 1;
const winnerName = room.getDuelPosPlayers(winner)[0]?.name || ''; const winnerPlayer = room.getDuelPosPlayers(winner)[0];
await room.sendChat( await room.sendChat(
`#{death2_finish_part1}${winnerName}#{death2_finish_part2}`, (sightPlayer) =>
`#{death2_finish_part1}${
winnerPlayer
? this.hidePlayerNameProvider.getHidPlayerName(
winnerPlayer,
sightPlayer,
)
: ''
}#{death2_finish_part2}`,
ChatColor.BABYBLUE, ChatColor.BABYBLUE,
); );
await room.win( await room.win(
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ 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';
...@@ -22,6 +23,7 @@ declare module '../room' { ...@@ -22,6 +23,7 @@ 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());
...@@ -148,7 +150,8 @@ export class SideTimeout { ...@@ -148,7 +150,8 @@ export class SideTimeout {
if (remainMinutes <= 1) { if (remainMinutes <= 1) {
this.clearSideTimeout(room, pos); this.clearSideTimeout(room, pos);
await room.sendChat( await room.sendChat(
`${client.name} #{side_overtime_room}`, (sightPlayer) =>
`${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,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ 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';
export interface WaitForPlayerConfig { export interface WaitForPlayerConfig {
...@@ -52,6 +53,7 @@ interface WaitForPlayerTickRuntime { ...@@ -52,6 +53,7 @@ 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;
...@@ -392,7 +394,8 @@ export class WaitForPlayerProvider { ...@@ -392,7 +394,8 @@ export class WaitForPlayerProvider {
) { ) {
room.waitForPlayerReadyWarnRemain = remainSeconds; room.waitForPlayerReadyWarnRemain = remainSeconds;
await room.sendChat( await room.sendChat(
`${target.name} ${remainSeconds} #{kick_count_down}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(target, sightPlayer)} ${remainSeconds} #{kick_count_down}`,
remainSeconds <= 9 ? ChatColor.RED : ChatColor.LIGHTBLUE, remainSeconds <= 9 ? ChatColor.RED : ChatColor.LIGHTBLUE,
); );
} }
...@@ -411,7 +414,8 @@ export class WaitForPlayerProvider { ...@@ -411,7 +414,8 @@ export class WaitForPlayerProvider {
return; return;
} }
await room.sendChat( await room.sendChat(
`${latestTarget.name} #{kicked_by_system}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(latestTarget, sightPlayer)} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await this.ctx.dispatch( await this.ctx.dispatch(
...@@ -462,7 +466,8 @@ export class WaitForPlayerProvider { ...@@ -462,7 +466,8 @@ 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(
`${waitingPlayer.name} #{kicked_by_system}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(waitingPlayer, sightPlayer)} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await this.ctx.dispatch( await this.ctx.dispatch(
...@@ -485,7 +490,8 @@ export class WaitForPlayerProvider { ...@@ -485,7 +490,8 @@ export class WaitForPlayerProvider {
); );
if (remainSeconds > 0) { if (remainSeconds > 0) {
await room.sendChat( await room.sendChat(
`${waitingPlayer.name} #{afk_warn_part1}${remainSeconds}#{afk_warn_part2}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(waitingPlayer, sightPlayer)} #{afk_warn_part1}${remainSeconds}#{afk_warn_part2}`,
ChatColor.RED, ChatColor.RED,
); );
} }
......
...@@ -2,10 +2,12 @@ import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode'; ...@@ -2,10 +2,12 @@ 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';
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
...@@ -76,7 +78,8 @@ export class LegacyBanService { ...@@ -76,7 +78,8 @@ export class LegacyBanService {
} }
await room.sendChat( await room.sendChat(
`${player.name} #{kicked_by_system}`, (sightPlayer) =>
`${this.hidePlayerNameProvider.getHidPlayerName(player, sightPlayer)} #{kicked_by_system}`,
ChatColor.RED, ChatColor.RED,
); );
await room.kick(player); await room.kick(player);
......
...@@ -959,11 +959,19 @@ export class Room { ...@@ -959,11 +959,19 @@ export class Room {
return this.sendChat(msg.msg, this.getIngamePos(client)); return this.sendChat(msg.msg, this.getIngamePos(client));
} }
async sendChat(msg: string, type: number = ChatColor.BABYBLUE) { async sendChat(
msg: string | ((p: Client) => Awaitable<string>),
type: number = ChatColor.BABYBLUE,
) {
if (this.finalizing) { if (this.finalizing) {
return; return;
} }
return Promise.all(this.allPlayers.map((p) => p.sendChat(msg, type))); return Promise.all(
this.allPlayers.map(async (p) => {
const resolvedMessage = typeof msg === 'function' ? await msg(p) : msg;
return p.sendChat(resolvedMessage, type);
}),
);
} }
firstgoPos?: number; firstgoPos?: number;
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
"*.ts", "*.ts",
"src/**/*.ts", "src/**/*.ts",
"test/**/*.ts", "test/**/*.ts",
"tests/**/*.ts" "tests/**/*.ts",
"plugins/**/*.ts"
] ]
} }
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