Commit f51e04d7 authored by nanahira's avatar nanahira

unfinished

parent 9182abd9
Pipeline #43129 passed with stages
in 2 minutes and 23 seconds
This diff is collapsed.
......@@ -12,6 +12,10 @@ import { YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { TcpServer } from './transport/tcp/server';
import { WsServer } from './transport/ws/server';
import { ClientVersionCheck } from './services/client-version-check';
import { AragamiService } from './services/aragami';
import { RoomManager } from './room/room-manager';
import { DefaultHostInfoProvider } from './room/default-hostinfo-provder';
import { YGOProResourceLoader } from './services/ygopro-resource-loader';
const core = createAppContext()
.provide(ConfigService, {
......@@ -20,6 +24,7 @@ const core = createAppContext()
.provide(Logger, { merge: ['createLogger'] })
.provide(Emitter, { merge: ['dispatch', 'middleware', 'removeMiddleware'] })
.provide(HttpClient, { merge: ['http'] })
.provide(AragamiService, { merge: ['aragami'] })
.define();
export type Context = typeof core;
......@@ -33,6 +38,9 @@ export const app = core
.provide(TcpServer)
.provide(WsServer)
.provide(ClientVersionCheck)
.provide(DefaultHostInfoProvider)
.provide(YGOProResourceLoader)
.provide(RoomManager)
.define();
app.middleware(YGOProCtosJoinGame, async (msg, client, _next) => {
......
......@@ -8,10 +8,15 @@ import {
YGOProStocChat,
ChatColor,
YGOProStocErrorMsg,
YGOProStocTypeChange,
YGOProStocHsPlayerEnter,
YGOProStocHsPlayerChange,
PlayerChangeState,
} from 'ygopro-msg-encode';
import { YGOProProtoPipe } from './utility/ygopro-proto-pipe';
import { I18nService } from './services/i18n';
import { Chnroute } from './services/chnroute';
import YGOProDeck from 'ygopro-deck-encode';
export abstract class Client {
protected abstract _send(data: Buffer): Promise<void>;
......@@ -20,6 +25,7 @@ export abstract class Client {
protected abstract _onDisconnect(): Observable<void>;
abstract physicalIp(): string;
// in handshake
ip = '';
isLocal = false;
......@@ -29,12 +35,14 @@ export abstract class Client {
constructor(protected ctx: Context) {}
receive$!: Observable<YGOProCtosBase>;
disconnect$!: Observable<void>;
disconnect$!: Observable<{ bySystem: boolean }>;
init() {
this.disconnect$ = merge(
this.disconnectSubject.asObservable(),
this._onDisconnect(),
this.disconnectSubject
.asObservable()
.pipe(map(() => ({ bySystem: true }))),
this._onDisconnect().pipe(map(() => ({ bySystem: false }))),
).pipe(take(1), share());
this.receive$ = this._receive().pipe(
YGOProProtoPipe(YGOProCtos, {
......@@ -62,7 +70,7 @@ export abstract class Client {
return this;
}
disconnect() {
disconnect(): undefined {
this.disconnectSubject.next();
this.disconnectSubject.complete();
this._disconnect().then();
......@@ -84,8 +92,8 @@ export abstract class Client {
return this.send(
new YGOProStocChat().fromPartial({
msg: await this.ctx
.get(I18nService)
.translate(this.ctx.get(Chnroute).getLocale(this.ip), msg),
.get(() => I18nService)
.translate(this.ctx.get(() => Chnroute).getLocale(this.ip), msg),
player_type: type,
}),
);
......@@ -108,10 +116,40 @@ export abstract class Client {
return this.ip || this.physicalIp() || 'unknown';
}
// in handshake
hostname = '';
name = '';
vpass = '';
name_vpass = '';
established = false;
// in room
isHost = false;
pos = -1;
deck?: YGOProDeck;
async sendTypeChange() {
return this.send(
new YGOProStocTypeChange().fromPartial({
isHost: this.isHost,
playerPosition: this.pos,
}),
);
}
prepareEnterPacket() {
return new YGOProStocHsPlayerEnter().fromPartial({
name: this.name,
pos: this.pos,
});
}
prepareChangePacket(state?: PlayerChangeState) {
return new YGOProStocHsPlayerChange().fromPartial({
playerPosition: this.pos,
playerState:
state ??
(this.deck ? PlayerChangeState.READY : PlayerChangeState.NOTREADY),
});
}
}
import yaml from 'yaml';
import * as fs from 'node:fs';
import { DefaultHostinfo } from './room/default-hostinfo';
import { Prettify } from 'nfkit';
export type HostinfoOptions = {
[K in keyof typeof DefaultHostinfo as `HOSTINFO_${Uppercase<K>}`]: string;
};
export const defaultConfig = {
HOST: '::',
......@@ -13,9 +19,17 @@ export const defaultConfig = {
NO_CONNECT_COUNT_LIMIT: '',
ALT_VERSIONS: '',
USE_PROXY: '',
YGOPRO_PATH: './ygopro',
EXTRA_SCRIPT_PATH: '',
...(Object.fromEntries(
Object.entries(DefaultHostinfo).map(([key, value]) => [
`HOSTINFO_${key.toUpperCase()}`,
value.toString(),
]),
) as HostinfoOptions),
};
export type Config = typeof defaultConfig;
export type Config = Prettify<typeof defaultConfig & HostinfoOptions>;
export function loadConfig(): Config {
let readConfig: Partial<Config> = {};
......
import { HostInfo } from 'ygopro-msg-encode';
import { Context } from '../app';
import { DefaultHostinfo } from './default-hostinfo';
export class DefaultHostInfoProvider {
constructor(private ctx: Context) {}
getHostinfo(): HostInfo {
const hostinfo = { ...DefaultHostinfo };
for (const key of Object.keys(hostinfo)) {
const configKey = `HOSTINFO_${key.toUpperCase()}`;
const value = this.ctx.getConfig(configKey as any) as string;
if (value) {
const num = Number(value);
if (!isNaN(num)) {
(hostinfo as any)[key] = num;
}
}
}
return hostinfo;
}
parseHostinfo(name: string, partial: Partial<HostInfo> = {}): HostInfo {
const defaultHostinfo = this.getHostinfo();
const hostinfo: HostInfo = {
...defaultHostinfo,
...partial,
};
if (name.startsWith('M#')) {
hostinfo.mode = 1;
return hostinfo;
}
if (name.startsWith('T#')) {
hostinfo.mode = 2;
hostinfo.start_lp = defaultHostinfo.start_lp * 2;
return hostinfo;
}
if (name.startsWith('AI#')) {
hostinfo.rule = 5;
hostinfo.lflist = -1;
hostinfo.time_limit = 0;
return hostinfo;
}
const compactParam = name.match(
/^(\d)(\d)([12345TF])(T|F)(T|F)(\d+),(\d+),(\d+)/i,
);
if (compactParam) {
hostinfo.rule = Number.parseInt(compactParam[1], 10);
hostinfo.mode = Number.parseInt(compactParam[2], 10);
const duelRuleNumber = Number.parseInt(compactParam[3], 10);
hostinfo.duel_rule = Number.isNaN(duelRuleNumber)
? compactParam[3].toUpperCase() === 'T'
? 3
: 5
: duelRuleNumber;
hostinfo.no_check_deck = compactParam[4].toUpperCase() === 'T' ? 1 : 0;
hostinfo.no_shuffle_deck = compactParam[5].toUpperCase() === 'T' ? 1 : 0;
hostinfo.start_lp = Number.parseInt(compactParam[6], 10);
hostinfo.start_hand = Number.parseInt(compactParam[7], 10);
hostinfo.draw_count = Number.parseInt(compactParam[8], 10);
return hostinfo;
}
const rulePrefix = name.match(/(.+)#/);
if (!rulePrefix) {
return hostinfo;
}
const rule = rulePrefix[1].toUpperCase();
if (/(^|,|,)(M|MATCH)(,|,|$)/.test(rule)) {
hostinfo.mode = 1;
}
if (/(^|,|,)(T|TAG)(,|,|$)/.test(rule)) {
hostinfo.mode = 2;
hostinfo.start_lp = defaultHostinfo.start_lp * 2;
}
if (/(^|,|,)(OOR|OCGONLYRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 0;
hostinfo.lflist = 0;
}
if (/(^|,|,)(OR|OCGRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 5;
hostinfo.lflist = 0;
}
if (/(^|,|,)(CR|CCGRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 2;
hostinfo.lflist = -1;
}
if (/(^|,|,)(TOR|TCGONLYRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 1;
}
if (/(^|,|,)(TR|TCGRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 5;
}
if (/(^|,|,)(OOMR|OCGONLYMATCHRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 0;
hostinfo.lflist = 0;
hostinfo.mode = 1;
}
if (/(^|,|,)(OMR|OCGMATCHRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 5;
hostinfo.lflist = 0;
hostinfo.mode = 1;
}
if (/(^|,|,)(CMR|CCGMATCHRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 2;
hostinfo.lflist = -1;
hostinfo.mode = 1;
}
if (/(^|,|,)(TOMR|TCGONLYMATCHRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 1;
hostinfo.mode = 1;
}
if (/(^|,|,)(TMR|TCGMATCHRANDOM)(,|,|$)/.test(rule)) {
hostinfo.rule = 5;
hostinfo.mode = 1;
}
if (/(^|,|,)(TCGONLY|TO)(,|,|$)/.test(rule)) {
hostinfo.rule = 1;
}
if (/(^|,|,)(OCGONLY|OO)(,|,|$)/.test(rule)) {
hostinfo.rule = 0;
hostinfo.lflist = 0;
}
if (/(^|,|,)(OT|TCG)(,|,|$)/.test(rule)) {
hostinfo.rule = 5;
}
if (/(^|,|,)(SC|CN|CCG|CHINESE)(,|,|$)/.test(rule)) {
hostinfo.rule = 2;
hostinfo.lflist = -1;
}
const lpParam = rule.match(/(^|,|,)LP(\d+)(,|,|$)/);
if (lpParam) {
let startLp = Number.parseInt(lpParam[2], 10);
if (startLp <= 0) startLp = 1;
if (startLp >= 99999) startLp = 99999;
hostinfo.start_lp = startLp;
}
const timeLimitParam = rule.match(/(^|,|,)(TIME|TM|TI)(\d+)(,|,|$)/);
if (timeLimitParam) {
let timeLimit = Number.parseInt(timeLimitParam[3], 10);
if (timeLimit < 0) timeLimit = 180;
if (timeLimit >= 1 && timeLimit <= 60) timeLimit *= 60;
if (timeLimit >= 999) timeLimit = 999;
hostinfo.time_limit = timeLimit;
}
const startHandParam = rule.match(/(^|,|,)(START|ST)(\d+)(,|,|$)/);
if (startHandParam) {
let startHand = Number.parseInt(startHandParam[3], 10);
if (startHand <= 0) startHand = 1;
if (startHand >= 40) startHand = 40;
hostinfo.start_hand = startHand;
}
const drawCountParam = rule.match(/(^|,|,)(DRAW|DR)(\d+)(,|,|$)/);
if (drawCountParam) {
let drawCount = Number.parseInt(drawCountParam[3], 10);
if (drawCount >= 35) drawCount = 35;
hostinfo.draw_count = drawCount;
}
const lflistParam = rule.match(/(^|,|,)(LFLIST|LF)(\d+)(,|,|$)/);
if (lflistParam) {
hostinfo.lflist = Number.parseInt(lflistParam[3], 10) - 1;
}
if (/(^|,|,)(NOLFLIST|NF)(,|,|$)/.test(rule)) {
hostinfo.lflist = -1;
}
if (/(^|,|,)(NOUNIQUE|NU)(,|,|$)/.test(rule)) {
hostinfo.rule = 4;
}
if (/(^|,|,)(CUSTOM|DIY)(,|,|$)/.test(rule)) {
hostinfo.rule = 3;
}
if (/(^|,|,)(NOCHECK|NC)(,|,|$)/.test(rule)) {
hostinfo.no_check_deck = 1;
}
if (/(^|,|,)(NOSHUFFLE|NS)(,|,|$)/.test(rule)) {
hostinfo.no_shuffle_deck = 1;
}
if (/(^|,|,)(IGPRIORITY|PR)(,|,|$)/.test(rule)) {
hostinfo.duel_rule = 4;
}
const duelRuleParam = rule.match(/(^|,|,)(DUELRULE|MR)(\d+)(,|,|$)/);
if (duelRuleParam) {
const duelRule = Number.parseInt(duelRuleParam[3], 10);
if (duelRule > 0 && duelRule <= 5) {
hostinfo.duel_rule = duelRule;
}
}
return hostinfo;
}
}
import { HostInfo } from 'ygopro-msg-encode';
export const DefaultHostinfo: HostInfo = {
lflist: 0, // lflist index
rule: 0, // 0: OCG, 1: TCG, 2: SC, 3: NOUNIQUE, 4: CUSTOM, 5: ALL
mode: 0, // 0: single, 1: match, 2: tag
duel_rule: 5, // 1-5
no_check_deck: 0,
no_shuffle_deck: 0,
start_lp: 8000,
start_hand: 5,
draw_count: 1,
time_limit: 240,
};
import { Context } from '../app';
import { Room } from './room';
export class RoomManager {
constructor(private ctx: Context) {}
private rooms = new Map<string, Room>();
findByName(name: string) {
return this.rooms.get(name);
}
allRooms() {
return Array.from(this.rooms.values());
}
async findOrCreateByName(name: string) {
const existing = this.findByName(name);
if (existing) return existing;
return this.ctx.aragami.lock(`room_create:${name}`, async () => {
const existing = this.findByName(name);
if (existing) return existing;
const room = await new Room(this.ctx, name)
.addFinalizor((r) => {
this.rooms.delete(r.name);
})
.init();
this.rooms.set(name, room);
return room;
});
}
}
import { Awaitable } from 'nfkit';
import { Context } from '../app';
import {
HostInfo,
NetPlayerType,
YGOProStocHsWatchChange,
YGOProStocJoinGame,
} from 'ygopro-msg-encode';
import { DefaultHostInfoProvider } from './default-hostinfo-provder';
import { CardReaderFinalized } from 'koishipro-core.js';
import { YGOProResourceLoader } from '../services/ygopro-resource-loader';
import { blankLFList } from '../utility/blank-lflist';
import { Client } from '../client';
export type RoomFinalizor = (self: Room) => Awaitable<any>;
export class Room {
constructor(
private ctx: Context,
public name: string,
private partialHostinfo: Partial<HostInfo> = {},
) {}
hostinfo = this.ctx
.get(() => DefaultHostInfoProvider)
.parseHostinfo(this.name, this.partialHostinfo);
players = new Array<Client>(this.hostinfo.mode === 2 ? 4 : 2);
watchers = new Set<Client>();
get allPlayers() {
return [...this.players.filter((p) => p), ...this.watchers];
}
private get resourceLoader() {
return this.ctx.get(() => YGOProResourceLoader);
}
private cardReader!: CardReaderFinalized;
private lflist = blankLFList;
private async findLFList() {
const isTCG = this.hostinfo.rule === 1 && this.hostinfo.lflist === 0;
let index = 0;
for await (const lflist of this.resourceLoader.getLFLists()) {
if (isTCG) {
if (lflist.name?.includes(' TCG')) {
return lflist;
}
} else {
if (index === this.hostinfo.lflist) {
return lflist;
} else if (index > this.hostinfo.lflist) {
return undefined;
}
}
++index;
}
}
async init() {
this.cardReader = await this.resourceLoader.getCardReader();
if (this.hostinfo.lflist >= 0) {
this.lflist = (await this.findLFList()) || blankLFList;
}
return this;
}
private finalizors: RoomFinalizor[] = [];
addFinalizor(finalizor: RoomFinalizor) {
this.finalizors.push(finalizor);
return this;
}
async finalize() {
while (this.finalizors.length) {
const finalizor = this.finalizors.pop()!;
await finalizor(this);
}
}
get joinGameMessage() {
return new YGOProStocJoinGame().fromPartial({
info: {
...this.hostinfo,
lflist: this.lflist === blankLFList ? 0 : this.lflist.getHash(),
},
});
}
get watcherSizeMessage() {
return new YGOProStocHsWatchChange().fromPartial({
watch_count: this.watchers.size,
});
}
async join(client: Client) {
client.disconnect$.subscribe(({ bySystem }) =>
this.onPlayerDisconnect(client, bySystem),
);
client.isHost = !this.allPlayers.length;
const firstEmptyPlayerSlot = this.players.findIndex((p) => !p);
if (firstEmptyPlayerSlot >= 0) {
this.players[firstEmptyPlayerSlot] = client;
client.pos = firstEmptyPlayerSlot;
} else {
this.watchers.add(client);
client.pos = NetPlayerType.OBSERVER;
}
await client.send(this.joinGameMessage);
await client.sendTypeChange();
for (let i = 0; i < this.players.length; ++i) {
const p = this.players[i];
if (p) {
await client.send(p.prepareEnterPacket());
await p.send(client.prepareEnterPacket());
if (p.deck) {
await client.send(p.prepareChangePacket());
}
}
}
if (this.watchers.size) {
await client.send(this.watcherSizeMessage);
}
}
async onPlayerDisconnect(client: Client, bySystem: boolean) {
if (client.pos === NetPlayerType.OBSERVER) {
this.watchers.delete(client);
for (const p of this.allPlayers) {
p.send(this.watcherSizeMessage).then();
}
return;
}
}
}
import { Aragami } from 'aragami';
import { AppContext } from 'nfkit';
export class AragamiService {
constructor(private ctx: AppContext) {}
aragami = new Aragami();
}
......@@ -2,6 +2,7 @@ import {
YGOProCtosBase,
YGOProCtosExternalAddress,
YGOProCtosJoinGame,
YGOProCtosLeaveGame,
YGOProCtosPlayerInfo,
} from 'ygopro-msg-encode';
import { Context } from '../app';
......@@ -12,45 +13,53 @@ import { forkJoin, filter, takeUntil, timeout, firstValueFrom } from 'rxjs';
export class ClientHandler {
constructor(private ctx: Context) {
this.ctx.middleware(
YGOProCtosExternalAddress,
async (msg, client, next) => {
this.ctx
.middleware(YGOProCtosExternalAddress, async (msg, client, next) => {
if (client instanceof WsClient || client.ip) {
// ws should tell real IP and hostname in http headers, so we skip this step for ws clients
return next();
}
this.ctx
.get(IpResolver)
.get(() => IpResolver)
.setClientIp(
client,
msg.real_ip === '0.0.0.0' ? undefined : msg.real_ip,
);
client.hostname = msg.hostname?.split(':')[0] || '';
return next();
},
);
this.ctx.middleware(YGOProCtosPlayerInfo, async (msg, client, next) => {
if (!client.ip) {
this.ctx.get(IpResolver).setClientIp(client);
}
const [name, vpass] = msg.name.split('$');
client.name = name;
client.vpass = vpass || '';
return next();
});
this.ctx.middleware(YGOProCtosBase, async (msg, client, next) => {
const isPreHandshakeMsg = [
YGOProCtosExternalAddress,
YGOProCtosPlayerInfo,
YGOProCtosJoinGame,
].some((allowed) => msg instanceof allowed);
if (client.established !== isPreHandshakeMsg) {
// disallow any messages before handshake is complete, except for the ones needed for handshake
return undefined;
}
return next();
});
})
.middleware(YGOProCtosPlayerInfo, async (msg, client, next) => {
if (!client.ip) {
this.ctx.get(() => IpResolver).setClientIp(client);
}
const [name, vpass] = msg.name.split('$');
client.name = name;
client.vpass = vpass || '';
return next();
})
.middleware(
YGOProCtosBase,
async (msg, client, next) => {
const isPreHandshakeMsg = [
YGOProCtosExternalAddress,
YGOProCtosPlayerInfo,
YGOProCtosJoinGame,
].some((allowed) => msg instanceof allowed);
if (client.established !== isPreHandshakeMsg) {
// disallow any messages before handshake is complete, except for the ones needed for handshake
return undefined;
}
return next();
},
true,
)
.middleware(
YGOProCtosLeaveGame, // this means immediately disconnect the client when they send leave game message, which is what official server does
async (msg, client, next) => {
return client.disconnect();
},
true,
);
}
private logger = this.ctx.createLogger('ClientHandler');
......
......@@ -4,15 +4,12 @@ import {
YGOProStocErrorMsg,
} from 'ygopro-msg-encode';
import { Context } from '../app';
import { convertNumberArray } from '../utility/convert-string-array';
const YGOPRO_VERSION = 0x1362;
export class ClientVersionCheck {
private altVersions = this.ctx
.getConfig('ALT_VERSIONS', '')
.split(',')
.map((v) => parseInt(v.trim()))
.filter((v) => v);
private altVersions = convertNumberArray(this.ctx.getConfig('ALT_VERSIONS'));
constructor(private ctx: Context) {
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
......
......@@ -6,6 +6,6 @@ import { ConfigService } from './config';
export class HttpClient {
constructor(private ctx: AppContext) {}
http = axios.create({
...useProxy(this.ctx.get(ConfigService).getConfig('USE_PROXY', '')),
...useProxy(this.ctx.get(() => ConfigService).getConfig('USE_PROXY', '')),
});
}
import { Context } from '../app';
import { Client } from '../client';
import * as ipaddr from 'ipaddr.js';
import { convertStringArray } from '../utility/convert-string-array';
export class IpResolver {
private logger = this.ctx.createLogger('IpResolver');
......@@ -9,16 +10,10 @@ export class IpResolver {
private trustedProxies: Array<[ipaddr.IPv4 | ipaddr.IPv6, number]> = [];
constructor(private ctx: Context) {
// Parse trusted proxies configuration
const trustedProxiesConfig = this.ctx.getConfig(
'TRUSTED_PROXIES',
'127.0.0.0/8,::1/128',
const proxies = convertStringArray(
this.ctx.getConfig('TRUSTED_PROXIES', '127.0.0.0/8,::1/128'),
);
const proxies = trustedProxiesConfig
.split(',')
.map((s) => s.trim())
.filter(Boolean);
for (const trusted of proxies) {
try {
this.trustedProxies.push(ipaddr.parseCIDR(trusted));
......
......@@ -5,7 +5,7 @@ import { ConfigService } from './config';
export class Logger {
constructor(private ctx: AppContext) {}
private readonly logger = pino({
level: this.ctx.get(ConfigService).getConfig('LOG_LEVEL') || 'info',
level: this.ctx.get(() => ConfigService).getConfig('LOG_LEVEL') || 'info',
transport: {
target: 'pino-pretty',
options: {
......
import initSqlJs, { SqlJsStatic } from 'sql.js';
import { Context } from '../app';
import { loadPaths } from '../utility/load-path';
import { DirCardReader, searchYGOProResource } from 'koishipro-core.js';
import { YGOProLFList, YGOProLFListItem } from 'ygopro-lflist-encode';
import path from 'node:path';
export class YGOProResourceLoader {
constructor(private ctx: Context) {}
ygoproPaths = loadPaths(this.ctx.getConfig('YGOPRO_PATH')).flatMap((p) => [
path.join(p, 'expansions'),
p,
]);
extraScriptPaths = loadPaths(this.ctx.getConfig('EXTRA_SCRIPT_PATH'));
private SQL!: SqlJsStatic;
async init() {
this.SQL = await initSqlJs();
}
async getCardReader() {
return DirCardReader(this.SQL, ...this.ygoproPaths);
}
async *getLFLists() {
for await (const file of searchYGOProResource(...this.ygoproPaths)) {
const filename = path.basename(file.path);
if (filename !== 'lflist.conf') {
continue;
}
const buf = await file.read();
const lflist = new YGOProLFList().fromText(
Buffer.from(buf).toString('utf-8'),
);
for (const item of lflist.items) {
yield item;
}
}
}
}
......@@ -38,7 +38,7 @@ export class TcpServer {
private handleConnection(socket: Socket): void {
const client = new TcpClient(this.ctx, socket);
const handler = this.ctx.get(ClientHandler);
const handler = this.ctx.get(() => ClientHandler);
handler.handleClient(client).catch((err) => {
this.logger.error({ err }, 'Error handling client');
});
......
......@@ -30,7 +30,7 @@ export class WsServer {
const portNum = parseInt(wsPort, 10);
// Try to get SSL configuration
const sslFinder = this.ctx.get(SSLFinder);
const sslFinder = this.ctx.get(() => SSLFinder);
const sslOptions = sslFinder.findSSL();
if (sslOptions) {
......@@ -67,9 +67,9 @@ export class WsServer {
private handleConnection(ws: WebSocket, req: IncomingMessage): void {
const client = new WsClient(this.ctx, ws, req);
if (this.ctx.get(IpResolver).setClientIp(client, client.xffIp())) return;
if (this.ctx.get(() => IpResolver).setClientIp(client, client.xffIp())) return;
client.hostname = req.headers.host?.split(':')[0] || '';
const handler = this.ctx.get(ClientHandler);
const handler = this.ctx.get(() => ClientHandler);
handler.handleClient(client).catch((err) => {
this.logger.error({ err }, 'Error handling client');
});
......
import { YGOProLFListItem } from 'ygopro-lflist-encode';
export const blankLFList = new YGOProLFListItem({
name: 'N/A',
});
export const convertStringArray = (str: string) =>
str
?.split(',')
.map((s) => s.trim())
.filter((s) => s) || [];
export const convertNumberArray = (str: string) =>
str
?.split(',')
.map((s) => parseInt(s.trim()))
.filter((n) => !isNaN(n)) || [];
import path from 'path';
import { convertStringArray } from './convert-string-array';
export const loadPaths = (pathStr: string) =>
convertStringArray(pathStr).map((p) => path.resolve(process.cwd(), p)) || [];
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