Commit 9efdf131 authored by nanahira's avatar nanahira

add leave events

parent 446b2b75
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"ipaddr.js": "^2.3.0", "ipaddr.js": "^2.3.0",
"koishipro-core.js": "^1.3.1", "koishipro-core.js": "^1.3.1",
"nfkit": "^1.0.24", "nfkit": "^1.0.24",
"p-queue": "6.6.2",
"pino": "^10.3.1", "pino": "^10.3.1",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
...@@ -3460,6 +3461,12 @@ ...@@ -3460,6 +3461,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/execa": { "node_modules/execa": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
...@@ -5460,6 +5467,15 @@ ...@@ -5460,6 +5467,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
...@@ -5492,6 +5508,34 @@ ...@@ -5492,6 +5508,34 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"license": "MIT",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": { "node_modules/p-try": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
......
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
"ipaddr.js": "^2.3.0", "ipaddr.js": "^2.3.0",
"koishipro-core.js": "^1.3.1", "koishipro-core.js": "^1.3.1",
"nfkit": "^1.0.24", "nfkit": "^1.0.24",
"p-queue": "6.6.2",
"pino": "^10.3.1", "pino": "^10.3.1",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
......
...@@ -17,6 +17,7 @@ import { YGOProProtoPipe } from '../utility/ygopro-proto-pipe'; ...@@ -17,6 +17,7 @@ import { YGOProProtoPipe } from '../utility/ygopro-proto-pipe';
import { I18nService } from '../services/i18n'; import { I18nService } from '../services/i18n';
import { Chnroute } from '../services/chnroute'; import { Chnroute } from '../services/chnroute';
import YGOProDeck from 'ygopro-deck-encode'; import YGOProDeck from 'ygopro-deck-encode';
import PQueue from 'p-queue';
export abstract class Client { export abstract class Client {
protected abstract _send(data: Buffer): Promise<void>; protected abstract _send(data: Buffer): Promise<void>;
...@@ -77,15 +78,19 @@ export abstract class Client { ...@@ -77,15 +78,19 @@ export abstract class Client {
return undefined; return undefined;
} }
private sendQueue = new PQueue({ concurrency: 1 });
async send(data: YGOProStocBase) { async send(data: YGOProStocBase) {
try { return this.sendQueue.add(async () => {
await this._send(Buffer.from(data.toFullPayload())); try {
} catch (e) { await this._send(Buffer.from(data.toFullPayload()));
this.logger.warn( } catch (e) {
{ ip: this.loggingIp(), error: (e as Error).message }, this.logger.warn(
`Failed to send message: ${(e as Error).message}`, { ip: this.loggingIp(), error: (e as Error).message },
); `Failed to send message: ${(e as Error).message}`,
} );
}
});
} }
async sendChat(msg: string, type = ChatColor.BABYBLUE) { async sendChat(msg: string, type = ChatColor.BABYBLUE) {
...@@ -128,6 +133,7 @@ export abstract class Client { ...@@ -128,6 +133,7 @@ export abstract class Client {
isHost = false; isHost = false;
pos = -1; pos = -1;
deck?: YGOProDeck; deck?: YGOProDeck;
startDeck?: YGOProDeck;
async sendTypeChange() { async sendTypeChange() {
return this.send( return this.send(
......
import { RoomEvent } from "./room-event";
export class OnRoomJoin extends RoomEvent { }
import { RoomEvent } from "./room-event";
export class OnRoomLeave extends RoomEvent { }
import { Room } from '../room';
import { RoomEvent } from './room-event';
export class OnRoomWin extends RoomEvent {
constructor(
room: Room,
public winPos: number,
public winMatch = false,
) {
super(room);
}
}
import { Room } from "../room";
export class RoomEvent {
constructor(public room: Room) {}
}
...@@ -3,6 +3,8 @@ import { Context } from '../app'; ...@@ -3,6 +3,8 @@ import { Context } from '../app';
import { import {
HostInfo, HostInfo,
NetPlayerType, NetPlayerType,
PlayerChangeState,
YGOProStocDuelStart,
YGOProStocHsWatchChange, YGOProStocHsWatchChange,
YGOProStocJoinGame, YGOProStocJoinGame,
} from 'ygopro-msg-encode'; } from 'ygopro-msg-encode';
...@@ -14,6 +16,9 @@ import { Client } from '../client/client'; ...@@ -14,6 +16,9 @@ import { Client } from '../client/client';
import { RoomMethod } from '../utility/decorators'; import { RoomMethod } from '../utility/decorators';
import { YGOProCtosDisconnect } from '../utility/ygopro-ctos-disconnect'; import { YGOProCtosDisconnect } from '../utility/ygopro-ctos-disconnect';
import { DuelStage } from './duel-stage'; import { DuelStage } from './duel-stage';
import { OnRoomJoin } from './room-event/on-room-join';
import { OnRoomLeave } from './room-event/on-room-leave';
import { OnRoomWin } from './room-event/on-room-win';
export type RoomFinalizor = (self: Room) => Awaitable<any>; export type RoomFinalizor = (self: Room) => Awaitable<any>;
...@@ -28,10 +33,17 @@ export class Room { ...@@ -28,10 +33,17 @@ export class Room {
.get(() => DefaultHostInfoProvider) .get(() => DefaultHostInfoProvider)
.parseHostinfo(this.name, this.partialHostinfo); .parseHostinfo(this.name, this.partialHostinfo);
get isTag() {
return this.hostinfo.mode === 2;
}
players = new Array<Client>(this.hostinfo.mode === 2 ? 4 : 2); players = new Array<Client>(this.hostinfo.mode === 2 ? 4 : 2);
watchers = new Set<Client>(); watchers = new Set<Client>();
get playingPlayers() {
return this.players.filter((p) => p);
}
get allPlayers() { get allPlayers() {
return [...this.players.filter((p) => p), ...this.watchers]; return [...this.playingPlayers, ...this.watchers];
} }
private get resourceLoader() { private get resourceLoader() {
...@@ -115,6 +127,40 @@ export class Room { ...@@ -115,6 +127,40 @@ export class Room {
}); });
} }
getTeammates(client: Client) {
if (client.pos === NetPlayerType.OBSERVER) {
return [];
}
if (this.isTag) {
const teamBit = (c: Client) => c.pos & 0x1;
return this.playingPlayers.filter((p) => teamBit(p) === teamBit(client));
}
return [];
}
getOpponents(client: Client) {
if (client.pos === NetPlayerType.OBSERVER) {
return [];
}
const teammates = new Set<Client>(this.getTeammates(client));
return this.playingPlayers.filter((p) => !teammates.has(p));
}
getDuelPos(client: Client) {
if (client.pos === NetPlayerType.OBSERVER) {
return -1;
}
const teamOffsetBit = this.isTag ? 1 : 0;
return (client.pos & (0x1 << teamOffsetBit)) >>> teamOffsetBit;
}
getPosPlayers(duelPos: number) {
if (duelPos === NetPlayerType.OBSERVER) {
return [...this.watchers];
}
return this.playingPlayers.filter((p) => this.getDuelPos(p) === duelPos);
}
async join(client: Client) { async join(client: Client) {
client.roomName = this.name; client.roomName = this.name;
client.isHost = !this.allPlayers.length; client.isHost = !this.allPlayers.length;
...@@ -126,34 +172,63 @@ export class Room { ...@@ -126,34 +172,63 @@ export class Room {
this.watchers.add(client); this.watchers.add(client);
client.pos = NetPlayerType.OBSERVER; client.pos = NetPlayerType.OBSERVER;
} }
await client.send(this.joinGameMessage);
await client.sendTypeChange(); // send to client
for (let i = 0; i < this.players.length; ++i) { client.send(this.joinGameMessage);
const p = this.players[i]; client.sendTypeChange();
if (p) { this.playingPlayers.forEach((p) => {
await client.send(p.prepareEnterPacket()); client.send(p.prepareEnterPacket());
await p.send(client.prepareEnterPacket()); // p.send(client.prepareEnterPacket());
if (p.deck) { if (p.deck) {
await client.send(p.prepareChangePacket()); client.send(p.prepareChangePacket());
}
} }
} });
if (this.watchers.size) { if (this.watchers.size) {
await client.send(this.watcherSizeMessage); client.send(this.watcherSizeMessage);
} }
// send to other players
this.allPlayers
.filter((p) => p !== client)
.forEach((p) => {
p.send(client.prepareEnterPacket());
});
await this.ctx.dispatch(new OnRoomJoin(this), client);
} }
duelStage = DuelStage.Begin; duelStage = DuelStage.Begin;
score = [0, 0];
async win(duelPos: number, winMatch = false) {
if (this.duelStage === DuelStage.Siding) {
this.playingPlayers
.filter((p) => p.deck)
.forEach((p) => p.send(new YGOProStocDuelStart()));
}
++this.score[duelPos];
// TODO: next game or finalize
await this.ctx.dispatch(
new OnRoomWin(this, duelPos, winMatch),
this.getPosPlayers(duelPos)[0],
);
}
@RoomMethod() @RoomMethod()
async onDisconnect(client: Client, _msg: YGOProCtosDisconnect) { async onDisconnect(client: Client, _msg: YGOProCtosDisconnect) {
if (client.pos === NetPlayerType.OBSERVER) { if (client.pos === NetPlayerType.OBSERVER) {
this.watchers.delete(client); this.watchers.delete(client);
for (const p of this.allPlayers) { for (const p of this.allPlayers) {
p.send(this.watcherSizeMessage).then(); p.send(this.watcherSizeMessage);
} }
} else { } else if (this.duelStage === DuelStage.Begin) {
this.players[client.pos] = undefined; this.players[client.pos] = undefined;
this.allPlayers.forEach((p) => {
p.send(client.prepareChangePacket(PlayerChangeState.LEAVE));
});
} else {
this.score[this.getDuelPos(client)] = -9;
await this.win(this.getDuelPos(client), true);
} }
if (client.isHost) { if (client.isHost) {
const nextHost = this.allPlayers.find((p) => p !== client); const nextHost = this.allPlayers.find((p) => p !== client);
...@@ -162,6 +237,8 @@ export class Room { ...@@ -162,6 +237,8 @@ export class Room {
await nextHost.sendTypeChange(); await nextHost.sendTypeChange();
} }
} }
await this.ctx.dispatch(new OnRoomLeave(this), client);
client.roomName = undefined; client.roomName = undefined;
} }
} }
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