Commit ab194281 authored by nanahira's avatar nanahira

send previous duels to post watch

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