Commit 4ecf82ee authored by nanahira's avatar nanahira

add onResponse

parent 74add160
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
"ygopro-cdb-encode": "^1.0.2", "ygopro-cdb-encode": "^1.0.2",
"ygopro-deck-encode": "^1.0.15", "ygopro-deck-encode": "^1.0.15",
"ygopro-lflist-encode": "^1.0.3", "ygopro-lflist-encode": "^1.0.3",
"ygopro-msg-encode": "^1.1.10", "ygopro-msg-encode": "^1.1.13",
"ygopro-yrp-encode": "^1.0.1", "ygopro-yrp-encode": "^1.0.1",
"yuzuthread": "^1.0.8" "yuzuthread": "^1.0.8"
}, },
...@@ -79,7 +79,6 @@ ...@@ -79,7 +79,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
...@@ -1606,7 +1605,6 @@ ...@@ -1606,7 +1605,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
...@@ -1690,7 +1688,6 @@ ...@@ -1690,7 +1688,6 @@
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/types": "8.55.0", "@typescript-eslint/types": "8.55.0",
...@@ -2171,7 +2168,6 @@ ...@@ -2171,7 +2168,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
...@@ -2568,7 +2564,6 @@ ...@@ -2568,7 +2564,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
...@@ -3229,7 +3224,6 @@ ...@@ -3229,7 +3224,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
...@@ -3286,7 +3280,6 @@ ...@@ -3286,7 +3280,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
...@@ -4163,7 +4156,6 @@ ...@@ -4163,7 +4156,6 @@
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.2.tgz",
"integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ioredis/commands": "1.5.0", "@ioredis/commands": "1.5.0",
"cluster-key-slot": "^1.1.0", "cluster-key-slot": "^1.1.0",
...@@ -4408,7 +4400,6 @@ ...@@ -4408,7 +4400,6 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "30.2.0", "@jest/core": "30.2.0",
"@jest/types": "30.2.0", "@jest/types": "30.2.0",
...@@ -5851,7 +5842,6 @@ ...@@ -5851,7 +5842,6 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
...@@ -6700,7 +6690,6 @@ ...@@ -6700,7 +6690,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
...@@ -6853,7 +6842,6 @@ ...@@ -6853,7 +6842,6 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
...@@ -6962,7 +6950,6 @@ ...@@ -6962,7 +6950,6 @@
"resolved": "https://registry.npmjs.org/typed-struct/-/typed-struct-2.7.1.tgz", "resolved": "https://registry.npmjs.org/typed-struct/-/typed-struct-2.7.1.tgz",
"integrity": "sha512-GluzA9kYlHjATJmzBDA2X9G9237Md5zsJsc8uEkmpvUFeuUvt+e7Sq11/nQnVB2VZIfKNR1CrwTCgpJVz52pAA==", "integrity": "sha512-GluzA9kYlHjATJmzBDA2X9G9237Md5zsJsc8uEkmpvUFeuUvt+e7Sq11/nQnVB2VZIfKNR1CrwTCgpJVz52pAA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
...@@ -6989,7 +6976,6 @@ ...@@ -6989,7 +6976,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
...@@ -7439,9 +7425,9 @@ ...@@ -7439,9 +7425,9 @@
} }
}, },
"node_modules/ygopro-msg-encode": { "node_modules/ygopro-msg-encode": {
"version": "1.1.10", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/ygopro-msg-encode/-/ygopro-msg-encode-1.1.10.tgz", "resolved": "https://registry.npmjs.org/ygopro-msg-encode/-/ygopro-msg-encode-1.1.13.tgz",
"integrity": "sha512-lRTbBwf3Gr6x1hIvTeojdbcWw91/UlbYAhjgPgH9RgUk+2Av18iq8hayKkZgpjqbYhdlHqyZj4aBMgDI+F5eJw==", "integrity": "sha512-wWRn6zH4kgg8vS2Z9CCzTAMwPe6WWjZLFizdtUEO6uY+uTNn+dMEIWQpQKHL4eAMXexNDZG1lVGm70eaIKeUkw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"typed-reflector": "^1.0.14", "typed-reflector": "^1.0.14",
......
...@@ -27,6 +27,7 @@ export const defaultConfig = { ...@@ -27,6 +27,7 @@ export const defaultConfig = {
DECK_EXTRA_MAX: '15', DECK_EXTRA_MAX: '15',
DECK_SIDE_MAX: '15', DECK_SIDE_MAX: '15',
DECK_MAX_COPIES: '3', DECK_MAX_COPIES: '3',
OCGCORE_DEBUG_LOG: '',
...(Object.fromEntries( ...(Object.fromEntries(
Object.entries(DefaultHostinfo).map(([key, value]) => [ Object.entries(DefaultHostinfo).map(([key, value]) => [
`HOSTINFO_${key.toUpperCase()}`, `HOSTINFO_${key.toUpperCase()}`,
......
...@@ -9,7 +9,6 @@ export class OcgcoreWorkerOptions { ...@@ -9,7 +9,6 @@ export class OcgcoreWorkerOptions {
hostinfo: HostInfo; hostinfo: HostInfo;
@TransportType(() => [YGOProDeck]) @TransportType(() => [YGOProDeck])
decks: YGOProDeck[]; decks: YGOProDeck[];
isTag?: boolean; isTag: boolean;
playerNames?: string[]; registry: Record<string, string>;
registry?: Record<string, string>;
} }
...@@ -32,7 +32,7 @@ import { OcgcoreWorkerOptions } from './ocgcore-worker-options'; ...@@ -32,7 +32,7 @@ import { OcgcoreWorkerOptions } from './ocgcore-worker-options';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { calculateDuelOptions } from '../utility/calculate-duel-options'; import { calculateDuelOptions } from '../utility/calculate-duel-options';
import initSqlJs from 'sql.js'; import initSqlJs from 'sql.js';
import { YGOProMessages, OcgcoreCommonConstants } from 'ygopro-msg-encode'; import { YGOProMessages } from 'ygopro-msg-encode';
const { OcgcoreScriptConstants } = _OcgcoreConstants; const { OcgcoreScriptConstants } = _OcgcoreConstants;
...@@ -305,43 +305,17 @@ export class OcgcoreWorker { ...@@ -305,43 +305,17 @@ export class OcgcoreWorker {
return this.duel.queryFieldInfo({ noParse: true }); return this.duel.queryFieldInfo({ noParse: true });
} }
@WorkerMethod() async *advance() {
@TransportEncoder<OcgcoreProcessResult[], SerializableProcessResult[]>(
// serialize in worker: only send raw
(results) =>
results.map((result) => ({
length: result.length,
raw: result.raw,
status: result.status,
})),
// deserialize in main thread: re-parse from raw
(serializedArray) =>
serializedArray.map((serialized) => ({
length: serialized.length,
raw: serialized.raw,
status: serialized.status,
message:
serialized.raw.length > 0
? (() => {
try {
return YGOProMessages.getInstanceFromPayload(serialized.raw);
} catch {
return undefined;
}
})()
: undefined,
})),
)
async advance(): Promise<OcgcoreProcessResult[]> {
const results: OcgcoreProcessResult[] = [];
while (true) { while (true) {
const res = this.duel.process({ noParse: true }); const res = await this.process();
results.push(res); if (!res.raw.length) {
continue;
}
yield res;
if (res.status > 0) { if (res.status > 0) {
break; break;
} }
} }
return results;
} }
@WorkerMethod() @WorkerMethod()
......
import YGOProDeck from 'ygopro-deck-encode'; import YGOProDeck from 'ygopro-deck-encode';
import { YGOProYrp, ReplayHeader } from 'ygopro-yrp-encode'; import { YGOProYrp, ReplayHeader } from 'ygopro-yrp-encode';
import { Room } from './room'; import { Room } from './room';
import { YGOProMsgBase } from 'ygopro-msg-encode';
// Constants from ygopro // Constants from ygopro
const REPLAY_COMPRESSED = 0x1; const REPLAY_COMPRESSED = 0x1;
...@@ -17,6 +18,7 @@ export class DuelRecord { ...@@ -17,6 +18,7 @@ export class DuelRecord {
date = new Date(); date = new Date();
winPosition?: number; winPosition?: number;
responses: Buffer[] = []; responses: Buffer[] = [];
messages: YGOProMsgBase[] = [];
toYrp(room: Room) { toYrp(room: Room) {
const isTag = room.isTag; const isTag = room.isTag;
......
import { Awaitable } from 'nfkit'; import { Awaitable, ProtoMiddlewareDispatcher } from 'nfkit';
import { Context } from '../app'; import { Context } from '../app';
import BetterLock from 'better-lock'; import BetterLock from 'better-lock';
import { import {
...@@ -35,9 +35,25 @@ import { ...@@ -35,9 +35,25 @@ import {
YGOProCtosHandResult, YGOProCtosHandResult,
YGOProStocHandResult, YGOProStocHandResult,
HandResult, HandResult,
YGOProMsgStart,
YGOProMsgNewTurn,
YGOProMsgNewPhase,
YGOProMsgBase,
YGOProMsgResponseBase,
YGOProMsgRetry,
RequireQueryLocation,
RequireQueryCardLocation,
YGOProMsgUpdateData,
YGOProMsgUpdateCard,
CardQuery,
YGOProCtosResponse,
} from 'ygopro-msg-encode'; } from 'ygopro-msg-encode';
import { DefaultHostInfoProvider } from './default-hostinfo-provder'; import { DefaultHostInfoProvider } from './default-hostinfo-provder';
import { CardReaderFinalized } from 'koishipro-core.js'; import {
CardReaderFinalized,
OcgcoreMessageType,
_OcgcoreConstants,
} from 'koishipro-core.js';
import { YGOProResourceLoader } from './ygopro-resource-loader'; import { YGOProResourceLoader } from './ygopro-resource-loader';
import { blankLFList } from '../utility/blank-lflist'; import { blankLFList } from '../utility/blank-lflist';
import { calculateDuelOptions } from '../utility/calculate-duel-options'; import { calculateDuelOptions } from '../utility/calculate-duel-options';
...@@ -59,6 +75,14 @@ import { checkDeck, checkChangeSide } from '../utility/check-deck'; ...@@ -59,6 +75,14 @@ import { checkDeck, checkChangeSide } from '../utility/check-deck';
import { DuelRecord } from './duel-record'; import { DuelRecord } from './duel-record';
import { generateSeed } from '../utility/generate-seed'; import { generateSeed } from '../utility/generate-seed';
import { OnRoomDuelStart } from './room-event/on-room-duel-start'; import { OnRoomDuelStart } from './room-event/on-room-duel-start';
import { OcgcoreWorker } from '../ocgcore-worker/ocgcore-worker';
import { initWorker } from 'yuzuthread';
import {
getZoneQueryFlag,
splitRefreshLocations,
} from '../utility/refresh-query';
const { OcgcoreScriptConstants } = _OcgcoreConstants;
export type RoomFinalizor = (self: Room) => Awaitable<any>; export type RoomFinalizor = (self: Room) => Awaitable<any>;
...@@ -231,7 +255,7 @@ export class Room { ...@@ -231,7 +255,7 @@ export class Room {
} }
isPosSwapped = false; isPosSwapped = false;
getSwappedPos(clientOrPos: Client | number) { getIngamePos(clientOrPos: Client | number) {
const pos = this.resolvePos(clientOrPos); const pos = this.resolvePos(clientOrPos);
if (pos === NetPlayerType.OBSERVER || !this.isPosSwapped) { if (pos === NetPlayerType.OBSERVER || !this.isPosSwapped) {
return pos; return pos;
...@@ -239,16 +263,16 @@ export class Room { ...@@ -239,16 +263,16 @@ export class Room {
return pos ^ (0x1 << this.teamOffsetBit); return pos ^ (0x1 << this.teamOffsetBit);
} }
getSwappedDuelPosByDuelPos(duelPos: number) { getIngameDuelPosByDuelPos(duelPos: number) {
if ([0, 1].includes(duelPos) && this.isPosSwapped) { if ([0, 1].includes(duelPos) && this.isPosSwapped) {
return 1 - duelPos; return 1 - duelPos;
} }
return duelPos; return duelPos;
} }
getSwappedDuelPos(clientOrPos: Client | number) { getIngameDuelPos(clientOrPos: Client | number) {
const duelPos = this.getDuelPos(clientOrPos); const duelPos = this.getDuelPos(clientOrPos);
return this.getSwappedDuelPosByDuelPos(duelPos); return this.getIngameDuelPosByDuelPos(duelPos);
} }
getDuelPosPlayers(duelPos: number) { getDuelPosPlayers(duelPos: number) {
...@@ -258,6 +282,11 @@ export class Room { ...@@ -258,6 +282,11 @@ export class Room {
return this.playingPlayers.filter((p) => this.getDuelPos(p) === duelPos); return this.playingPlayers.filter((p) => this.getDuelPos(p) === duelPos);
} }
getIngameDuelPosPlayers(duelPos: number) {
const swappedDuelPos = this.getIngameDuelPosByDuelPos(duelPos);
return this.getDuelPosPlayers(swappedDuelPos);
}
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;
...@@ -341,13 +370,17 @@ export class Room { ...@@ -341,13 +370,17 @@ export class Room {
} }
} }
get lastDuelRecord() {
return this.duelRecords[this.duelRecords.length - 1];
}
async win(winMsg: Partial<YGOProMsgWin>, forceWinMatch = false) { async win(winMsg: Partial<YGOProMsgWin>, forceWinMatch = false) {
if (this.duelStage === DuelStage.Siding) { if (this.duelStage === DuelStage.Siding) {
this.playingPlayers this.playingPlayers
.filter((p) => !p.deck) .filter((p) => !p.deck)
.forEach((p) => p.send(new YGOProStocDuelStart())); .forEach((p) => p.send(new YGOProStocDuelStart()));
} }
const duelPos = this.getSwappedDuelPosByDuelPos(winMsg.player!); const duelPos = this.getIngameDuelPosByDuelPos(winMsg.player!);
this.isPosSwapped = false; this.isPosSwapped = false;
await Promise.all( await Promise.all(
this.allPlayers.map((p) => this.allPlayers.map((p) =>
...@@ -362,7 +395,7 @@ export class Room { ...@@ -362,7 +395,7 @@ export class Room {
...winMsg, ...winMsg,
player: duelPos, player: duelPos,
}); });
const lastDuelRecord = this.duelRecords[this.duelRecords.length - 1]; const lastDuelRecord = this.lastDuelRecord;
if (lastDuelRecord) { if (lastDuelRecord) {
lastDuelRecord.winPosition = duelPos; lastDuelRecord.winPosition = duelPos;
} }
...@@ -418,7 +451,7 @@ export class Room { ...@@ -418,7 +451,7 @@ export class Room {
} else { } else {
this.score[this.getDuelPos(client)] = -9; this.score[this.getDuelPos(client)] = -9;
await this.win( await this.win(
{ player: this.getSwappedDuelPos(client), type: 0x4 }, { player: this.getIngameDuelPos(client), type: 0x4 },
true, true,
); );
} }
...@@ -723,7 +756,7 @@ export class Room { ...@@ -723,7 +756,7 @@ export class Room {
@RoomMethod() @RoomMethod()
private async onChat(client: Client, msg: YGOProCtosChat) { private async onChat(client: Client, msg: YGOProCtosChat) {
return this.sendChat(msg.msg, this.getSwappedPos(client)); return this.sendChat(msg.msg, this.getIngamePos(client));
} }
async sendChat(msg: string, type: number = ChatColor.BABYBLUE) { async sendChat(msg: string, type: number = ChatColor.BABYBLUE) {
...@@ -872,6 +905,12 @@ export class Room { ...@@ -872,6 +905,12 @@ export class Room {
return true; return true;
} }
private ocgcore?: OcgcoreWorker;
private registry: Record<string, string> = {};
turnCount = 0;
turnPos = 0;
phase = undefined;
@RoomMethod({ allowInDuelStages: DuelStage.FirstGo }) @RoomMethod({ allowInDuelStages: DuelStage.FirstGo })
private async onDuelStart(client: Client, msg: YGOProCtosTpResult) { private async onDuelStart(client: Client, msg: YGOProCtosTpResult) {
if (client !== this.firstgoPlayer) { if (client !== this.firstgoPlayer) {
...@@ -885,7 +924,7 @@ export class Room { ...@@ -885,7 +924,7 @@ export class Room {
); );
if (this.isPosSwapped) { if (this.isPosSwapped) {
this.playingPlayers.forEach((p) => { this.playingPlayers.forEach((p) => {
duelRecord.players[this.getSwappedDuelPos(p)] = { duelRecord.players[this.getIngameDuelPos(p)] = {
name: p.name, name: p.name,
deck: p.deck!, deck: p.deck!,
}; };
...@@ -893,12 +932,339 @@ export class Room { ...@@ -893,12 +932,339 @@ export class Room {
} }
this.duelRecords.push(duelRecord); this.duelRecords.push(duelRecord);
const extraScriptPaths = [
'./script/patches/entry.lua',
'./script/special.lua',
'./script/init.lua',
...this.resourceLoader.extraScriptPaths,
];
const isMatchMode = this.winMatchCount > 1;
const duelMode = this.isTag ? 'tag' : isMatchMode ? 'match' : 'single';
const registry: Record<string, string> = {
...this.registry,
duel_mode: duelMode,
start_lp: String(this.hostinfo.start_lp),
start_hand: String(this.hostinfo.start_hand),
draw_count: String(this.hostinfo.draw_count),
player_type_0: this.isPosSwapped ? '1' : '0',
player_type_1: this.isPosSwapped ? '0' : '1',
};
if (isMatchMode) {
// Match mode uses completed duel count in gframe (before current duel result).
registry.duel_count = String(this.duelRecords.length - 1);
}
duelRecord.players.forEach((player, i) => {
registry[`player_name_${i}`] = player.name;
});
this.ocgcore = await initWorker(OcgcoreWorker, {
seed: duelRecord.seed,
hostinfo: this.hostinfo,
ygoproPaths: this.resourceLoader.ygoproPaths,
extraScriptPaths,
registry,
decks: duelRecord.players.map((p) => p.deck),
isTag: this.isTag,
});
const [
player0DeckCount,
player0ExtraCount,
player1DeckCount,
player1ExtraCount,
] = await Promise.all([
this.ocgcore.queryFieldCount({
player: 0,
location: OcgcoreScriptConstants.LOCATION_DECK,
}),
this.ocgcore.queryFieldCount({
player: 0,
location: OcgcoreScriptConstants.LOCATION_EXTRA,
}),
this.ocgcore.queryFieldCount({
player: 1,
location: OcgcoreScriptConstants.LOCATION_DECK,
}),
this.ocgcore.queryFieldCount({
player: 1,
location: OcgcoreScriptConstants.LOCATION_EXTRA,
}),
]);
const createStartMsg = (playerType: number) =>
new YGOProStocGameMsg().fromPartial({
msg: new YGOProMsgStart().fromPartial({
playerType,
duelRule: this.hostinfo.duel_rule,
startLp0: this.hostinfo.start_lp,
startLp1: this.hostinfo.start_lp,
player0: {
deckCount: player0DeckCount,
extraCount: player0ExtraCount,
},
player1: {
deckCount: player1DeckCount,
extraCount: player1ExtraCount,
},
}),
});
const duelPos0Clients = this.getIngameDuelPosPlayers(0);
const duelPos1Clients = this.getIngameDuelPosPlayers(1);
const watcherMsg = createStartMsg(this.isPosSwapped ? 0x11 : 0x10);
await Promise.all([
...duelPos0Clients.map((p) => p.send(createStartMsg(0))),
...duelPos1Clients.map((p) => p.send(createStartMsg(1))),
...[...this.watchers].map((p) => p.send(watcherMsg)),
]);
this.duelStage = DuelStage.Dueling;
this.ocgcore.message$.subscribe((msg) => {
if (
msg.type === OcgcoreMessageType.DebugMessage &&
!this.ctx.getConfig('OCGCORE_DEBUG_LOG', '')
) {
return;
}
this.allPlayers.forEach((p) => p.sendChat(`Debug: ${msg.message}`));
});
this.ocgcore.registry$.subscribe((registry) => {
Object.assign(this.registry, registry);
});
this.turnCount = 0;
this.turnPos = 0;
this.phase = undefined;
await this.handleGameMsg(watcherMsg.msg);
await this.ctx.dispatch( await this.ctx.dispatch(
new OnRoomDuelStart(this), new OnRoomDuelStart(this),
this.getDuelPosPlayers(this.getSwappedDuelPos(0))[0], this.getOpreatingPlayer(this.turnPos),
);
return this.advance();
}
private async onNewTurn(tp: number) {
++this.turnCount;
this.turnPos = tp;
}
private async onNewPhase(phase: number) {
this.phase = phase;
}
getOpreatingPlayer(duelPos: number): Client | undefined {
const players = this.getIngameDuelPosPlayers(duelPos);
if (!this.isTag) {
return players[0];
}
if (players.length === 1) {
return players[0];
}
// tag_duel.cpp cur_player equivalent, computed from turnCount:
// duelPos 0: start from players[0], toggle every two turns from turn 3
// duelPos 1: start from players[1], toggle every two turns from turn 2
const tc = Math.max(0, this.turnCount);
if (duelPos === 0) {
const idx = Math.floor(Math.max(0, tc - 1) / 2) % 2;
return players[idx];
}
if (duelPos === 1) {
const idx = 1 - (Math.floor(tc / 2) % 2);
return players[idx];
}
return players[0];
}
private async refreshLocations(refresh: RequireQueryLocation) {
if (!this.ocgcore) {
return;
}
const locations = splitRefreshLocations(refresh.location);
for (const location of locations) {
const { cards } = await this.ocgcore.queryFieldCard({
player: refresh.player,
location,
queryFlag: getZoneQueryFlag(location),
useCache: 1,
});
await this.handleGameMsg(
new YGOProMsgUpdateData().fromPartial({
player: refresh.player,
location,
cards: cards ?? [],
}),
true,
);
}
}
private async refreshSingle(refresh: RequireQueryCardLocation) {
if (!this.ocgcore) {
return;
}
const locations = splitRefreshLocations(refresh.location);
for (const location of locations) {
const { card } = await this.ocgcore.queryCard({
player: refresh.player,
location,
sequence: refresh.sequence,
queryFlag:
0xf81fff |
OcgcoreCommonConstants.QUERY_CODE |
OcgcoreCommonConstants.QUERY_POSITION,
useCache: 0,
});
await this.handleGameMsg(
new YGOProMsgUpdateCard().fromPartial({
controller: refresh.player,
location,
sequence: refresh.sequence,
card:
card ??
(() => {
const empty = new CardQuery();
empty.flags = 0;
empty.empty = true;
return empty;
})(),
}),
true,
);
}
}
private async routeGameMsg(message: YGOProMsgBase) {
const sendTargets = message.getSendTargets();
const sendGameMsg = (c: Client, msg: YGOProMsgBase) =>
c.send(new YGOProStocGameMsg().fromPartial({ msg }));
await Promise.all(
sendTargets.map(async (pos) => {
if (pos === NetPlayerType.OBSERVER) {
const observerView = message.observerView();
await Promise.all(
[...this.watchers].map((w) => sendGameMsg(w, observerView)),
);
} else {
const playerView = message.playerView(pos);
const players = this.getIngameDuelPosPlayers(pos);
const operatingPlayer = this.getOpreatingPlayer(pos);
await Promise.all(
players.map((c) =>
sendGameMsg(
c,
c === operatingPlayer ? playerView : playerView.teammateView(),
),
),
); );
}
}),
);
await Promise.all([
...message.getRequireRefreshCards().map((loc) => this.refreshSingle(loc)),
...message
.getRequireRefreshZones()
.map((loc) => this.refreshLocations(loc)),
]);
}
private async handleGameMsg(message: YGOProMsgBase, route = false) {
await this.localGameMsgDispatcher.dispatch(message);
await this.ctx.dispatch(message, this.getOpreatingPlayer(this.turnPos));
if (!route) {
await this.routeGameMsg(message);
}
}
localGameMsgDispatcher = new ProtoMiddlewareDispatcher()
.middleware(YGOProMsgNewTurn, async (message, next) => {
// check new turn
const player = message.player;
if (!(player & 0x2)) {
await this.onNewTurn(player & 0x1);
}
return next();
})
.middleware(YGOProMsgNewPhase, async (message, next) => {
// check new phase
await this.onNewPhase(message.phase);
return next();
})
.middleware(YGOProMsgBase, async (message, next) => {
// record messages for replay
if (!(message instanceof YGOProMsgResponseBase)) {
this.lastDuelRecord.messages.push(message);
}
return next();
})
.middleware(YGOProMsgBase, async (message, next) => {
//
if (this.pendingResponse && !(message instanceof YGOProMsgRetry)) {
// player made valid response
const resp = this.pendingResponse;
this.pendingResponse = undefined;
this.lastDuelRecord.responses.push(resp);
// TODO: clear timer
}
return next();
})
.middleware(YGOProMsgResponseBase, async (message, next) => {
this.responsePos = message.responsePlayer();
// TODO: set timer
return next();
});
private pendingResponse?: Buffer;
private responsePos?: number;
private async advance() {
if (!this.ocgcore) {
return;
}
this.allPlayers.forEach((p) => p.sendChat('TODO: duel start')); try {
for await (const { status, message } of this.ocgcore.advance()) {
if (!message) {
this.logger.warn({ message }, 'Received empty message from ocgcore');
if (status) {
throw new Error(
'Cannot continue ocgcore because received empty message with non-advancing status ' +
status,
);
}
}
await this.handleGameMsg(message);
if (message instanceof YGOProMsgWin) {
return this.win(message);
}
await this.routeGameMsg(message);
}
} catch (e) {
this.logger.warn({ error: e }, 'Error while advancing ocgcore');
return this.finalize(); return this.finalize();
} }
}
@RoomMethod({
allowInDuelStages: DuelStage.Dueling,
})
private async onResponse(client: Client, msg: YGOProCtosResponse) {
if (
this.responsePos == null ||
client !== this.getOpreatingPlayer(this.responsePos) ||
!this.ocgcore
) {
return;
}
this.pendingResponse = Buffer.from(msg.response);
this.responsePos = undefined;
await this.ocgcore.setResponse(msg.response);
return this.advance();
}
} }
import { AppContext, ProtoMiddlewareDispatcher } from 'nfkit'; import { AppContext, ProtoMiddlewareDispatcher } from 'nfkit';
import { Client } from '../client/client'; import { Client } from '../client/client';
export class Emitter extends ProtoMiddlewareDispatcher<[Client]> { export class Emitter extends ProtoMiddlewareDispatcher<[client: Client]> {
constructor(private ctx: AppContext) { constructor(private ctx: AppContext) {
super({ super({
acceptResult: () => true, acceptResult: () => true,
......
import { _OcgcoreConstants } from 'koishipro-core.js';
const { OcgcoreScriptConstants } = _OcgcoreConstants;
export function splitRefreshLocations(location: number) {
const bits = [
OcgcoreScriptConstants.LOCATION_MZONE,
OcgcoreScriptConstants.LOCATION_SZONE,
OcgcoreScriptConstants.LOCATION_HAND,
OcgcoreScriptConstants.LOCATION_GRAVE,
OcgcoreScriptConstants.LOCATION_REMOVED,
OcgcoreScriptConstants.LOCATION_EXTRA,
OcgcoreScriptConstants.LOCATION_DECK,
OcgcoreScriptConstants.LOCATION_OVERLAY,
OcgcoreScriptConstants.LOCATION_FZONE,
OcgcoreScriptConstants.LOCATION_PZONE,
];
const locations = bits.filter((bit) => (location & bit) !== 0);
if (locations.length > 0) {
return locations;
}
return [location];
}
export function getZoneQueryFlag(location: number) {
if (location === OcgcoreScriptConstants.LOCATION_MZONE) {
return 0x881fff;
}
if (location === OcgcoreScriptConstants.LOCATION_SZONE) {
return 0xe81fff;
}
if (location === OcgcoreScriptConstants.LOCATION_HAND) {
return 0x681fff;
}
if (location === OcgcoreScriptConstants.LOCATION_GRAVE) {
return 0x081fff;
}
if (location === OcgcoreScriptConstants.LOCATION_EXTRA) {
return 0xe81fff;
}
return 0xf81fff;
}
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