Commit ab194281 authored by nanahira's avatar nanahira

send previous duels to post watch

parent 70f2b880
import YGOProDeck from 'ygopro-deck-encode';
import { import {
ChatColor, ChatColor,
HostInfo, HostInfo,
NetPlayerType,
YGOProMsgResponseBase,
YGOProMsgWin,
YGOProStocDuelEnd, YGOProStocDuelEnd,
YGOProStocDuelStart, YGOProStocDuelStart,
YGOProStocGameMsg, YGOProStocGameMsg,
...@@ -20,10 +16,6 @@ import { MenuEntry, MenuManager } from '../menu-manager'; ...@@ -20,10 +16,6 @@ import { MenuEntry, MenuManager } from '../menu-manager';
import { DuelRecordEntity } from './duel-record.entity'; import { DuelRecordEntity } from './duel-record.entity';
import { DuelRecordPlayer } from './duel-record-player.entity'; import { DuelRecordPlayer } from './duel-record-player.entity';
import { import {
decodeDeckBase64,
decodeMessagesBase64,
decodeResponsesBase64,
decodeSeedBase64,
encodeCurrentDeckBase64, encodeCurrentDeckBase64,
encodeDeckBase64, encodeDeckBase64,
encodeIngameDeckBase64, encodeIngameDeckBase64,
...@@ -395,25 +387,25 @@ export class CloudReplayService { ...@@ -395,25 +387,25 @@ export class CloudReplayService {
viewMode: ReplayWatchViewMode = 'default', viewMode: ReplayWatchViewMode = 'default',
) { ) {
try { try {
const duelRecord = replay.toDuelRecord();
await client.sendChat( await client.sendChat(
`#{cloud_replay_playing} R#${replay.id}`, `#{cloud_replay_playing} R#${replay.id}`,
ChatColor.BABYBLUE, ChatColor.BABYBLUE,
); );
await client.send(this.createJoinGamePacket(replay)); await client.send(this.createJoinGamePacket(replay));
await this.sendReplayPlayers(client, replay); await this.sendReplayPlayers(client, duelRecord);
await client.send(new YGOProStocDuelStart()); await client.send(new YGOProStocDuelStart());
const gameMessages = this.resolveReplayVisibleMessages( const gameMessages = this.resolveReplayVisibleMessages(
replay.messages, duelRecord,
viewMode, viewMode,
); );
for (const msg of gameMessages) { for (const msg of gameMessages) {
await client.send(msg); await client.send(msg);
} }
await this.sendReplayWinMsg(client, replay);
if (withYrp) { if (withYrp) {
await client.send(this.createReplayPacket(replay)); await client.send(this.createReplayPacket(replay.hostInfo, duelRecord));
} }
await client.send(new YGOProStocDuelEnd()); await client.send(new YGOProStocDuelEnd());
...@@ -432,84 +424,16 @@ export class CloudReplayService { ...@@ -432,84 +424,16 @@ export class CloudReplayService {
} }
private resolveReplayVisibleMessages( private resolveReplayVisibleMessages(
messagesBase64: string, duelRecord: DuelRecord,
viewMode: ReplayWatchViewMode = 'default', viewMode: ReplayWatchViewMode = 'default',
) { ) {
const visiblePackets = decodeMessagesBase64(messagesBase64).filter( return duelRecord.toObserverPlayback(
(packet) => { viewMode === 'default'
const msg = packet.msg; ? (msg) => msg
if (!msg) { : viewMode === 'observer'
return false; ? (msg) => msg.observerView()
} : (msg) => msg.playerView(viewMode === 'player0' ? 0 : 1),
if (msg instanceof YGOProMsgResponseBase) {
return false;
}
if (msg instanceof YGOProMsgWin) {
return false;
}
return msg.getSendTargets().includes(NetPlayerType.OBSERVER);
},
);
if (viewMode === 'default') {
return visiblePackets;
}
return visiblePackets.map((packet) => {
const sourceMsg = packet.msg;
let mappedMsg = sourceMsg;
if (viewMode === 'player0') {
mappedMsg = sourceMsg.playerView(0);
} else if (viewMode === 'player1') {
mappedMsg = sourceMsg.playerView(1);
} else if (viewMode === 'observer') {
mappedMsg = sourceMsg.observerView();
}
return new YGOProStocGameMsg().fromPartial({
msg: mappedMsg,
});
});
}
private async sendReplayWinMsg(client: Client, replay: DuelRecordEntity) {
const player = this.resolveReplayWinPlayer(replay);
if (player == null) {
return;
}
await client.send(
new YGOProStocGameMsg().fromPartial({
msg: new YGOProMsgWin().fromPartial({
player,
type: replay.winReason,
}),
}),
);
}
private resolveReplayWinPlayer(replay: DuelRecordEntity) {
const winnerPlayer = replay.players.find((player) => player.winner);
if (!winnerPlayer) {
return 0x2; // PLAYER_NONE
}
const winnerDuelPos = this.resolveDuelPosBySeat(
winnerPlayer.pos,
replay.hostInfo,
); );
const swapped = this.resolveReplaySwappedByIsFirst(replay);
return swapped ? 1 - winnerDuelPos : winnerDuelPos;
}
private resolveDuelPosBySeat(pos: number, hostInfo: HostInfo) {
const teamOffsetBit = this.isTagMode(hostInfo) ? 1 : 0;
return (pos & (0x1 << teamOffsetBit)) >>> teamOffsetBit;
}
private resolveReplaySwappedByIsFirst(replay: DuelRecordEntity) {
const pos0Player = replay.players.find((player) => player.pos === 0);
return !pos0Player?.isFirst;
} }
private async downloadReplayYrp( private async downloadReplayYrp(
...@@ -518,11 +442,12 @@ export class CloudReplayService { ...@@ -518,11 +442,12 @@ export class CloudReplayService {
withJoinGame = false, withJoinGame = false,
) { ) {
try { try {
const duelRecord = replay.toDuelRecord();
if (withJoinGame) { if (withJoinGame) {
await client.send(this.createJoinGamePacket(replay)); await client.send(this.createJoinGamePacket(replay));
} }
await client.send(new YGOProStocDuelStart()); await client.send(new YGOProStocDuelStart());
await client.send(this.createReplayPacket(replay)); await client.send(this.createReplayPacket(replay.hostInfo, duelRecord));
await client.send(new YGOProStocDuelEnd()); await client.send(new YGOProStocDuelEnd());
client.disconnect(); client.disconnect();
} catch (error) { } catch (error) {
...@@ -538,11 +463,11 @@ export class CloudReplayService { ...@@ -538,11 +463,11 @@ export class CloudReplayService {
} }
} }
private async sendReplayPlayers(client: Client, replay: DuelRecordEntity) { private async sendReplayPlayers(client: Client, duelRecord: DuelRecord) {
const seatCount = this.resolveSeatCount(replay.hostInfo); const seatCount = duelRecord.players.length;
const sortedPlayers = [...replay.players].sort((a, b) => a.pos - b.pos); const sortedPlayers = [...duelRecord.players];
for (let pos = 0; pos < seatCount; pos += 1) { for (let pos = 0; pos < seatCount; pos += 1) {
const player = sortedPlayers.find((entry) => entry.pos === pos); const player = sortedPlayers[pos];
await client.send( await client.send(
new YGOProStocHsPlayerEnter().fromPartial({ new YGOProStocHsPlayerEnter().fromPartial({
pos, pos,
...@@ -567,18 +492,18 @@ export class CloudReplayService { ...@@ -567,18 +492,18 @@ export class CloudReplayService {
}; };
} }
private createReplayPacket(replay: DuelRecordEntity) { private createReplayPacket(hostInfo: HostInfo, duelRecord: DuelRecord) {
const duelRecord = this.restoreDuelRecord(replay);
return new YGOProStocReplay().fromPartial({ return new YGOProStocReplay().fromPartial({
replay: duelRecord.toYrp({ replay: duelRecord.toYrp({
hostinfo: replay.hostInfo as any, hostinfo: hostInfo as any,
isTag: this.isTagMode(replay.hostInfo), isTag: this.isTagMode(hostInfo),
}), }),
}); });
} }
buildReplayYrpPayload(replay: DuelRecordEntity) { buildReplayYrpPayload(replay: DuelRecordEntity) {
return this.createReplayPacket(replay).replay.toYrp(); const duelRecord = replay.toDuelRecord();
return this.createReplayPacket(replay.hostInfo, duelRecord).replay.toYrp();
} }
async getReplayYrpPayloadById(replayId: number) { async getReplayYrpPayloadById(replayId: number) {
...@@ -589,38 +514,6 @@ export class CloudReplayService { ...@@ -589,38 +514,6 @@ export class CloudReplayService {
return this.buildReplayYrpPayload(replay); return this.buildReplayYrpPayload(replay);
} }
private restoreDuelRecord(replay: DuelRecordEntity) {
const wasSwapped = this.resolveReplaySwappedByIsFirst(replay);
const seatCount = this.resolveSeatCount(replay.hostInfo);
const players = Array.from({ length: seatCount }, () => ({
name: '',
deck: new YGOProDeck(),
}));
const sortedPlayers = [...replay.players].sort((a, b) => a.pos - b.pos);
for (const player of sortedPlayers) {
const deckBuffer = player.ingameDeckBuffer || player.currentDeckBuffer;
const mainc = player.ingameDeckMainc ?? player.currentDeckMainc ?? 0;
if (player.pos < 0 || player.pos >= seatCount) {
continue;
}
players[player.pos] = {
name: player.name,
deck: decodeDeckBase64(deckBuffer, mainc),
};
}
const duelRecord = new DuelRecord(
decodeSeedBase64(replay.seed),
players,
wasSwapped,
);
duelRecord.startTime = replay.startTime;
duelRecord.endTime = replay.endTime;
duelRecord.responses = decodeResponsesBase64(replay.responses);
return duelRecord;
}
private async getReplayPage(client: Client): Promise<ReplayPage> { private async getReplayPage(client: Client): Promise<ReplayPage> {
const cursor = this.getReplayCursor(client); const cursor = this.getReplayCursor(client);
const firstPage = this.isFirstReplayPage(client); const firstPage = this.isFirstReplayPage(client);
...@@ -826,10 +719,6 @@ export class CloudReplayService { ...@@ -826,10 +719,6 @@ export class CloudReplayService {
return (hostInfo.mode & 0x2) !== 0; return (hostInfo.mode & 0x2) !== 0;
} }
private resolveSeatCount(hostInfo: HostInfo) {
return this.isTagMode(hostInfo) ? 4 : 2;
}
private parseDirectReplayPass(pass: string): DirectReplayPass | undefined { private parseDirectReplayPass(pass: string): DirectReplayPass | undefined {
if (pass.startsWith('W0#')) { if (pass.startsWith('W0#')) {
return { return {
......
import { HostInfo } from 'ygopro-msg-encode'; import { HostInfo } from 'ygopro-msg-encode';
import YGOProDeck from 'ygopro-deck-encode';
import { import {
Column, Column,
Entity, Entity,
...@@ -8,7 +9,14 @@ import { ...@@ -8,7 +9,14 @@ import {
PrimaryColumn, PrimaryColumn,
} from 'typeorm'; } from 'typeorm';
import { BaseTimeEntity, BigintTransformer } from '../../utility'; import { BaseTimeEntity, BigintTransformer } from '../../utility';
import { DuelRecord } from '../../room';
import { DuelRecordPlayer } from './duel-record-player.entity'; import { DuelRecordPlayer } from './duel-record-player.entity';
import {
decodeDeckBase64,
decodeMessagesBase64,
decodeResponsesBase64,
decodeSeedBase64,
} from './utility';
@Entity('duel_record') @Entity('duel_record')
export class DuelRecordEntity extends BaseTimeEntity { export class DuelRecordEntity extends BaseTimeEntity {
...@@ -78,4 +86,71 @@ export class DuelRecordEntity extends BaseTimeEntity { ...@@ -78,4 +86,71 @@ export class DuelRecordEntity extends BaseTimeEntity {
cascade: true, cascade: true,
}) })
players!: DuelRecordPlayer[]; players!: DuelRecordPlayer[];
toDuelRecord() {
const seatCount = this.resolveSeatCount();
const players = Array.from({ length: seatCount }, () => ({
name: '',
deck: new YGOProDeck(),
}));
const sortedPlayers = [...(this.players || [])].sort(
(a, b) => a.pos - b.pos,
);
for (const player of sortedPlayers) {
const deckBuffer = player.ingameDeckBuffer || player.currentDeckBuffer;
const mainc = player.ingameDeckMainc ?? player.currentDeckMainc ?? 0;
if (player.pos < 0 || player.pos >= seatCount) {
continue;
}
players[player.pos] = {
name: player.name,
deck: decodeDeckBase64(deckBuffer, mainc),
};
}
const duelRecord = new DuelRecord(
decodeSeedBase64(this.seed),
players,
this.resolveSwappedByIsFirst(),
);
duelRecord.startTime = this.startTime;
duelRecord.endTime = this.endTime;
duelRecord.winPosition = this.resolveWinPosition();
duelRecord.winReason = this.winReason;
duelRecord.messages = decodeMessagesBase64(this.messages).map(
(packet) => packet.msg,
);
duelRecord.responses = decodeResponsesBase64(this.responses);
return duelRecord;
}
private resolveWinPosition() {
const winnerPlayer = (this.players || []).find((player) => player.winner);
if (!winnerPlayer) {
return undefined;
}
return this.resolveDuelPosBySeat(winnerPlayer.pos);
}
private resolveSwappedByIsFirst() {
const pos0Player = (this.players || []).find((player) => player.pos === 0);
if (!pos0Player) {
return false;
}
return !pos0Player.isFirst;
}
private resolveDuelPosBySeat(pos: number) {
const teamOffsetBit = this.isTagMode() ? 1 : 0;
return (pos & (0x1 << teamOffsetBit)) >>> teamOffsetBit;
}
private isTagMode() {
return (this.hostInfo.mode & 0x2) !== 0;
}
private resolveSeatCount() {
return this.isTagMode() ? 4 : 2;
}
} }
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'; import {
NetPlayerType,
YGOProMsgBase,
YGOProMsgResponseBase,
YGOProMsgWin,
YGOProStocGameMsg,
} from 'ygopro-msg-encode';
import { calculateDuelOptions } from '../utility/calculate-duel-options'; import { calculateDuelOptions } from '../utility/calculate-duel-options';
// Constants from ygopro // Constants from ygopro
...@@ -20,6 +26,7 @@ export class DuelRecord { ...@@ -20,6 +26,7 @@ export class DuelRecord {
startTime = new Date(); startTime = new Date();
endTime?: Date; endTime?: Date;
winPosition?: number; winPosition?: number;
winReason?: number;
responses: Buffer[] = []; responses: Buffer[] = [];
messages: YGOProMsgBase[] = []; messages: YGOProMsgBase[] = [];
...@@ -106,4 +113,53 @@ export class DuelRecord { ...@@ -106,4 +113,53 @@ export class DuelRecord {
return yrp; return yrp;
} }
*toObserverPlayback(
cb: (msg: YGOProMsgBase) => YGOProMsgBase | undefined = (msg) => msg,
): Generator<YGOProStocGameMsg, void, unknown> {
let recordedWinMsg: YGOProMsgWin | undefined;
for (const message of this.messages) {
if (message instanceof YGOProMsgResponseBase) {
continue;
}
if (message instanceof YGOProMsgWin) {
if (!recordedWinMsg) {
recordedWinMsg = message;
}
continue;
}
if (!message.getSendTargets().includes(NetPlayerType.OBSERVER)) {
continue;
}
const mappedMsg = cb(message);
if (!mappedMsg) {
continue;
}
yield new YGOProStocGameMsg().fromPartial({
msg: mappedMsg,
});
}
const winMsg = this.resolveObserverWinMsg() || recordedWinMsg;
if (winMsg) {
yield new YGOProStocGameMsg().fromPartial({
msg: winMsg,
});
}
}
private resolveObserverWinMsg() {
if (
(this.winPosition !== 0 && this.winPosition !== 1) ||
typeof this.winReason !== 'number'
) {
return undefined;
}
const player = this.isSwapped ? 1 - this.winPosition : this.winPosition;
return new YGOProMsgWin().fromPartial({
player,
type: this.winReason,
});
}
} }
...@@ -352,9 +352,20 @@ export class Room { ...@@ -352,9 +352,20 @@ export class Room {
private async sendPostWatchMessages(client: Client) { private async sendPostWatchMessages(client: Client) {
await client.send(new YGOProStocDuelStart()); await client.send(new YGOProStocDuelStart());
const previousDuels = this.duelRecords.slice(0, -1);
if (previousDuels.length) {
for (const duelRecord of previousDuels) {
for (const message of duelRecord.toObserverPlayback((msg) =>
msg.observerView(),
)) {
await client.send(message);
}
}
}
// 在 SelectHand / SelectTp 阶段发送 DeckCount // 在 SelectHand / SelectTp 阶段发送 DeckCount
// Siding 阶段不发 DeckCount // Siding 阶段不发 DeckCount
if ( else if (
this.duelStage === DuelStage.Finger || this.duelStage === DuelStage.Finger ||
this.duelStage === DuelStage.FirstGo this.duelStage === DuelStage.FirstGo
) { ) {
...@@ -365,18 +376,10 @@ export class Room { ...@@ -365,18 +376,10 @@ export class Room {
await client.send(new YGOProStocWaitingSide()); await client.send(new YGOProStocWaitingSide());
} else if (this.duelStage === DuelStage.Dueling) { } else if (this.duelStage === DuelStage.Dueling) {
// Dueling 阶段不发 DeckCount,直接发送观战消息 // Dueling 阶段不发 DeckCount,直接发送观战消息
const observerMessages = for (const message of this.lastDuelRecord?.toObserverPlayback((msg) =>
this.lastDuelRecord?.messages.filter( msg.observerView(),
(msg) => ) || []) {
!(msg instanceof YGOProMsgResponseBase) && await client.send(message);
msg.getSendTargets().includes(NetPlayerType.OBSERVER),
) || [];
for (const message of observerMessages) {
await client.send(
new YGOProStocGameMsg().fromPartial({
msg: message.observerView(),
}),
);
} }
} }
} }
...@@ -545,6 +548,7 @@ export class Room { ...@@ -545,6 +548,7 @@ export class Room {
const lastDuelRecord = this.lastDuelRecord; const lastDuelRecord = this.lastDuelRecord;
if (lastDuelRecord && this.duelStage === DuelStage.Dueling) { if (lastDuelRecord && this.duelStage === DuelStage.Dueling) {
lastDuelRecord.winPosition = duelPos; lastDuelRecord.winPosition = duelPos;
lastDuelRecord.winReason = winMsg.type;
lastDuelRecord.endTime = new Date(); lastDuelRecord.endTime = new Date();
} }
if (typeof forceWinMatch === 'number') { if (typeof forceWinMatch === 'number') {
......
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