Commit 68164fb4 authored by nanahira's avatar nanahira

refa structure

parent 19dbe06b
HOST: "::"
PORT: "7911"
REDIS_URL: ""
LOG_LEVEL: info
WS_PORT: "0"
SSL_PATH: ""
......@@ -9,3 +10,15 @@ TRUSTED_PROXIES: 127.0.0.0/8,::1/128
NO_CONNECT_COUNT_LIMIT: ""
ALT_VERSIONS: ""
USE_PROXY: ""
YGOPRO_PATH: ./ygopro
EXTRA_SCRIPT_PATH: ""
HOSTINFO_LFLIST: "0"
HOSTINFO_RULE: "0"
HOSTINFO_MODE: "0"
HOSTINFO_DUEL_RULE: "5"
HOSTINFO_NO_CHECK_DECK: "0"
HOSTINFO_NO_SHUFFLE_DECK: "0"
HOSTINFO_START_LP: "8000"
HOSTINFO_START_HAND: "5"
HOSTINFO_DRAW_COUNT: "1"
HOSTINFO_TIME_LIMIT: "240"
......@@ -11,11 +11,12 @@
"dependencies": {
"aragami": "^1.2.10",
"axios": "^1.13.5",
"better-lock": "^3.2.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"ipaddr.js": "^2.3.0",
"koishipro-core.js": "^1.3.1",
"nfkit": "^1.0.24",
"nfkit": "^1.0.27",
"p-queue": "6.6.2",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
......@@ -2310,6 +2311,12 @@
"typed-reflector": "^1.0.12"
}
},
"node_modules/aragami/node_modules/better-lock": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/better-lock/-/better-lock-2.0.3.tgz",
"integrity": "sha512-3bCaToLrmEXZcIOOVWgi1STvp3/6EpoZAmlWBeuX2MvDB0Ql2ctl/vQ0CbhQIJYQiptdGypllP3ez+TeEmdnKQ==",
"license": "MIT"
},
"node_modules/aragami/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
......@@ -2511,9 +2518,9 @@
}
},
"node_modules/better-lock": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/better-lock/-/better-lock-2.0.3.tgz",
"integrity": "sha512-3bCaToLrmEXZcIOOVWgi1STvp3/6EpoZAmlWBeuX2MvDB0Ql2ctl/vQ0CbhQIJYQiptdGypllP3ez+TeEmdnKQ==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/better-lock/-/better-lock-3.2.0.tgz",
"integrity": "sha512-Wm6j7yKyEjXBCIhjRTVOEScyxOtwFNCuGnQ63bRIwialPQSGcltmMc319t4AWuI8xr/INSfAs7I6x9oN+o2fqA==",
"license": "MIT"
},
"node_modules/brace-expansion": {
......@@ -5373,9 +5380,9 @@
"license": "MIT"
},
"node_modules/nfkit": {
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/nfkit/-/nfkit-1.0.24.tgz",
"integrity": "sha512-Av73fUWuEU2rF8Lyng/Y8YIDUDHjeDHqInyKsS0Me+hWlW0TvqIhmK/DMpFS7aXxBf+Fp0btecIVoxXYUmAlnQ==",
"version": "1.0.27",
"resolved": "https://registry.npmjs.org/nfkit/-/nfkit-1.0.27.tgz",
"integrity": "sha512-xgsVsp1aHgrkvxWeTb6Zv55kji9Hq3KcFTZqaIDtQmBBgDwcRtmC2o2/BTQ4ZYjzhaopX+uJCFacfRn39xfL2w==",
"license": "MIT"
},
"node_modules/node-int64": {
......
import { createAppContext } from 'nfkit';
import { AppContextState, createAppContext } from 'nfkit';
import { ConfigService } from './services/config';
import { Logger } from './services/logger';
import { Emitter } from './services/emitter';
import { SSLFinder } from './services/ssl-finder';
import { ClientHandler } from './client/client-handler';
import { IpResolver } from './services/ip-resolver';
import { HttpClient } from './services/http-client';
import { Chnroute } from './services/chnroute';
import { I18nService } from './services/i18n';
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 { RoomEventRegister } from './room/room-event-register';
import { DefaultHostInfoProvider } from './room/default-hostinfo-provder';
import { YGOProResourceLoader } from './services/ygopro-resource-loader';
import { TransportModule } from './client/transport-module';
import { JoinHandlerModule } from './join-handlers/join-handler-module';
import { RoomModule } from './room/room-module';
import { SqljsFactory, SqljsLoader } from './services/sqljs';
const core = createAppContext()
.provide(ConfigService, {
......@@ -26,31 +17,17 @@ const core = createAppContext()
.provide(Emitter, { merge: ['dispatch', 'middleware', 'removeMiddleware'] })
.provide(HttpClient, { merge: ['http'] })
.provide(AragamiService, { merge: ['aragami'] })
.provide(SqljsLoader, {
useFactory: SqljsFactory,
merge: ['SQL'],
})
.define();
export type Context = typeof core;
export type ContextState = AppContextState<Context>;
export const app = core
.provide(SSLFinder)
.provide(IpResolver)
.provide(Chnroute)
.provide(I18nService)
.provide(ClientHandler)
.provide(TcpServer)
.provide(WsServer)
.provide(ClientVersionCheck)
.provide(DefaultHostInfoProvider)
.provide(YGOProResourceLoader)
.provide(RoomManager)
.provide(RoomEventRegister)
.use(TransportModule)
.use(RoomModule)
.use(JoinHandlerModule)
.define();
app.middleware(YGOProCtosJoinGame, async (msg, client, _next) => {
await client.sendChat(`Welcome ${client.name_vpass || client.name}!`);
await client.sendChat(`Your IP: ${client.ip}`);
await client.sendChat(`Your physical IP: ${client.physicalIp()}`);
await client.sendChat(`Your pass: ${msg.pass}`);
return client.die(
'This server is for testing purposes only. Please use an official server to play the game.',
);
});
......@@ -6,8 +6,8 @@ import {
} from 'ygopro-msg-encode';
import { Context } from '../app';
import { Client } from './client';
import { IpResolver } from '../services/ip-resolver';
import { WsClient } from '../transport/ws/client';
import { IpResolver } from './ip-resolver';
import { WsClient } from './transport/ws/client';
import {
forkJoin,
filter,
......
......@@ -12,10 +12,11 @@ import {
YGOProStocHsPlayerEnter,
YGOProStocHsPlayerChange,
PlayerChangeState,
NetPlayerType,
} from 'ygopro-msg-encode';
import { YGOProProtoPipe } from '../utility/ygopro-proto-pipe';
import { I18nService } from '../services/i18n';
import { Chnroute } from '../services/chnroute';
import { I18nService } from './i18n';
import { Chnroute } from './chnroute';
import YGOProDeck from 'ygopro-deck-encode';
import PQueue from 'p-queue';
......@@ -93,12 +94,18 @@ export abstract class Client {
});
}
async sendChat(msg: string, type = ChatColor.BABYBLUE) {
async sendChat(msg: string, type: number = ChatColor.BABYBLUE) {
return this.send(
new YGOProStocChat().fromPartial({
msg: await this.ctx
.get(() => I18nService)
.translate(this.ctx.get(() => Chnroute).getLocale(this.ip), msg),
msg:
type <= NetPlayerType.OBSERVER
? msg
: await this.ctx
.get(() => I18nService)
.translate(
this.ctx.get(() => Chnroute).getLocale(this.ip),
msg,
),
player_type: type,
}),
);
......
import { Context } from '../app';
import { Client } from '../client/client';
import { Client } from './client';
import * as ipaddr from 'ipaddr.js';
import { convertStringArray } from '../utility/convert-string-array';
......
import { createAppContext } from 'nfkit';
import { ContextState } from '../app';
import { TcpServer } from './transport/tcp/server';
import { WsServer } from './transport/ws/server';
import { ClientHandler } from './client-handler';
import { Chnroute } from './chnroute';
import { I18nService } from './i18n';
import { IpResolver } from './ip-resolver';
import { SSLFinder } from './ssl-finder';
export const TransportModule = createAppContext<ContextState>()
.provide(SSLFinder)
.provide(IpResolver)
.provide(Chnroute)
.provide(I18nService)
.provide(ClientHandler)
.provide(TcpServer)
.provide(WsServer);
import { Socket } from 'node:net';
import { Observable, fromEvent, merge } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { Context } from '../../app';
import { Client } from '../../client/client';
import { Context } from '../../../app';
import { Client } from '../../client';
export class TcpClient extends Client {
constructor(
......
import { Server as NetServer, Socket, createServer } from 'node:net';
import { Context } from '../../app';
import { ClientHandler } from '../../client/client-handler';
import { Context } from '../../../app';
import { ClientHandler } from '../../client-handler';
import { TcpClient } from './client';
export class TcpServer {
......
......@@ -3,8 +3,8 @@ import { Socket } from 'node:net';
import { Observable, fromEvent, merge } from 'rxjs';
import { map, take } from 'rxjs/operators';
import WebSocket, { RawData } from 'ws';
import { Context } from '../../app';
import { Client } from '../../client/client';
import { Context } from '../../../app';
import { Client } from '../../client';
export class WsClient extends Client {
constructor(
......
import { IncomingMessage, createServer as createHttpServer } from 'node:http';
import { createServer as createHttpsServer } from 'node:https';
import { Server as WebSocketServer } from 'ws';
import { Context } from '../../app';
import { ClientHandler } from '../../client/client-handler';
import { SSLFinder } from '../../services/ssl-finder';
import { Context } from '../../../app';
import { ClientHandler } from '../../client-handler';
import { SSLFinder } from '../../ssl-finder';
import { WsClient } from './client';
import { WebSocket } from 'ws';
import { IpResolver } from '../../services/ip-resolver';
import { IpResolver } from '../../ip-resolver';
export class WsServer {
private wss?: WebSocketServer;
......
......@@ -10,6 +10,7 @@ export type HostinfoOptions = {
export const defaultConfig = {
HOST: '::',
PORT: '7911',
REDIS_URL: '',
LOG_LEVEL: 'info',
WS_PORT: '0',
SSL_PATH: '',
......
......@@ -7,6 +7,7 @@ export const TRANSLATIONS = {
'Your client version is not fully supported. Please rejoin to enable temporary compatibility mode. For the best experience, we recommend updating your game to the latest version.',
version_polyfilled:
'Temporary compatibility mode has been enabled for your version. We recommend updating your game to avoid potential compatibility issues in the future.',
blank_room_name: 'Blank room name is unallowed, please fill in something.',
},
'zh-CN': {
update_required: '请更新你的客户端版本',
......@@ -15,5 +16,6 @@ export const TRANSLATIONS = {
'当前客户端版本暂未完全支持。请重新加入以启用临时兼容模式。为获得更佳体验,建议尽快更新游戏版本。',
version_polyfilled:
'已为当前版本启用临时兼容模式。建议尽快更新游戏,以避免后续兼容性问题。',
blank_room_name: '房间名不能为空,请在主机密码处填写房间名',
},
};
import { YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app';
export class JoinFallback {
constructor(private ctx: Context) {
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
return client.die('#{blank_room_name}');
});
}
}
import { createAppContext } from 'nfkit';
import { ContextState } from '../app';
import { ClientVersionCheck } from './client-version-check';
import { JoinRoom } from './join-room';
export const JoinHandlerModule = createAppContext<ContextState>()
.provide(ClientVersionCheck)
.provide(JoinRoom)
.define();
import { YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app';
import { RoomManager } from '../room/room-manager';
export class JoinRoom {
constructor(private ctx: Context) {
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
if (!msg.pass) {
return next();
}
const roomManager = this.ctx.get(() => RoomManager);
const room = await roomManager.findOrCreateByName(msg.pass);
return room.join(client);
});
}
}
import { RoomEvent } from "./room-event";
import { RoomEvent } from './room-event';
export class OnRoomJoin extends RoomEvent { }
export class OnRoomJoin extends RoomEvent {}
import { RoomEvent } from "./room-event";
import { RoomEvent } from './room-event';
export class OnRoomLeave extends RoomEvent { }
export class OnRoomLeave extends RoomEvent {}
import { Room } from "../room";
import { Room } from '../room';
export class RoomEvent {
constructor(public room: Room) {}
......
import { Context } from '../app';
import { Room, RoomFinalizor } from './room';
import BetterLock from 'better-lock';
export class RoomManager {
constructor(private ctx: Context) {}
......@@ -24,11 +25,13 @@ export class RoomManager {
return Array.from(this.rooms.values());
}
private roomCreateLock = new BetterLock();
async findOrCreateByName(name: string) {
const existing = this.findByName(name);
if (existing) return existing;
return this.ctx.aragami.lock(`room_create:${name}`, async () => {
return this.roomCreateLock.acquire(`room_create:${name}`, async () => {
const existing = this.findByName(name);
if (existing) return existing;
......
import { createAppContext } from 'nfkit';
import { ContextState } from '../app';
import { YGOProResourceLoader } from './ygopro-resource-loader';
import { DefaultHostInfoProvider } from './default-hostinfo-provder';
import { RoomEventRegister } from './room-event-register';
import { RoomManager } from './room-manager';
export const RoomModule = createAppContext<ContextState>()
.provide(DefaultHostInfoProvider)
.provide(YGOProResourceLoader)
.provide(RoomManager)
.provide(RoomEventRegister);
......@@ -16,10 +16,12 @@ import {
YGOProStocDeckCount_DeckInfo,
YGOProStocSelectTp,
YGOProStocSelectHand,
ChatColor,
YGOProCtosChat,
} from 'ygopro-msg-encode';
import { DefaultHostInfoProvider } from './default-hostinfo-provder';
import { CardReaderFinalized } from 'koishipro-core.js';
import { YGOProResourceLoader } from '../services/ygopro-resource-loader';
import { YGOProResourceLoader } from './ygopro-resource-loader';
import { blankLFList } from '../utility/blank-lflist';
import { Client } from '../client/client';
import { RoomMethod } from '../utility/decorators';
......@@ -163,12 +165,15 @@ export class Room {
return this.playingPlayers.filter((p) => !teammates.has(p));
}
private get teamOffsetBit() {
return this.isTag ? 1 : 0;
}
getDuelPos(client: Client) {
if (client.pos === NetPlayerType.OBSERVER) {
return -1;
}
const teamOffsetBit = this.isTag ? 1 : 0;
return (client.pos & (0x1 << teamOffsetBit)) >>> teamOffsetBit;
return (client.pos & (0x1 << this.teamOffsetBit)) >>> this.teamOffsetBit;
}
getPosPlayers(duelPos: number) {
......@@ -178,6 +183,14 @@ export class Room {
return this.playingPlayers.filter((p) => this.getDuelPos(p) === duelPos);
}
isPosSwapped = false;
getSwappedPos(client: Client) {
if (client.pos === NetPlayerType.OBSERVER || !this.isPosSwapped) {
return client.pos;
}
return client.pos ^ (0x1 << this.teamOffsetBit);
}
async join(client: Client) {
client.roomName = this.name;
client.isHost = !this.allPlayers.length;
......@@ -221,6 +234,7 @@ export class Room {
} else {
await this.ctx.dispatch(new OnRoomJoinObserver(this), client);
}
return undefined;
}
duelStage = DuelStage.Begin;
......@@ -432,6 +446,15 @@ export class Room {
this.allPlayers.forEach((p) => p.send(changeMsg));
}
@RoomMethod()
private async onChat(client: Client, msg: YGOProCtosChat) {
return this.sendChat(msg.msg, this.getSwappedPos(client));
}
async sendChat(msg: string, type: number = ChatColor.BABYBLUE) {
return Promise.all(this.allPlayers.map((p) => p.sendChat(msg, type)));
}
duelCount = 0;
firstgoPlayer?: Client;
......
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 { YGOProLFList } from 'ygopro-lflist-encode';
import path from 'node:path';
export class YGOProResourceLoader {
......@@ -14,14 +13,8 @@ export class YGOProResourceLoader {
]);
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);
return DirCardReader(this.ctx.SQL, ...this.ygoproPaths);
}
async *getLFLists() {
......
import { Aragami } from 'aragami';
import { AppContext } from 'nfkit';
import { ConfigService } from './config';
export class AragamiService {
constructor(private ctx: AppContext) {}
aragami = new Aragami();
private redisUrl = this.ctx.get(ConfigService).getConfig('REDIS_URL');
aragami = new Aragami({
redis: this.redisUrl ? { uri: this.redisUrl } : undefined,
});
}
import { AppContext } from 'nfkit';
import type { SqlJsStatic } from 'sql.js';
import initSqlJs from 'sql.js';
export class SqljsLoader {
constructor(private ctx: AppContext) {}
SQL!: SqlJsStatic;
setSqlJs(SQL: SqlJsStatic) {
this.SQL = SQL;
return this;
}
}
export const SqljsFactory = async (ctx: AppContext) => {
const SQL = await initSqlJs();
return new SqljsLoader(ctx).setSqlJs(SQL);
};
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