Commit 8caf4b7d authored by nanahira's avatar nanahira

reconnect feature

parent 302b5d5d
Pipeline #43202 passed with stages
in 1 minute and 31 seconds
...@@ -20,6 +20,8 @@ DECK_SIDE_MAX: "15" ...@@ -20,6 +20,8 @@ DECK_SIDE_MAX: "15"
DECK_MAX_COPIES: "3" DECK_MAX_COPIES: "3"
OCGCORE_DEBUG_LOG: "" OCGCORE_DEBUG_LOG: ""
WELCOME: "" WELCOME: ""
NO_RECONNECT: ""
RECONNECT_TIMEOUT: "180000"
HOSTINFO_LFLIST: "0" HOSTINFO_LFLIST: "0"
HOSTINFO_RULE: "0" HOSTINFO_RULE: "0"
HOSTINFO_MODE: "0" HOSTINFO_MODE: "0"
......
import { filter, merge, Observable, of, Subject } from 'rxjs'; import { filter, merge, Observable, of, Subject } from 'rxjs';
import { map, share, take, takeUntil } from 'rxjs/operators'; import { map, share, take, takeUntil, tap } from 'rxjs/operators';
import { Context } from '../app'; import { Context } from '../app';
import { import {
YGOProCtos, YGOProCtos,
...@@ -55,7 +55,13 @@ export class Client { ...@@ -55,7 +55,13 @@ export class Client {
.asObservable() .asObservable()
.pipe(map(() => ({ bySystem: true }))), .pipe(map(() => ({ bySystem: true }))),
this._onDisconnect().pipe(map(() => ({ bySystem: false }))), this._onDisconnect().pipe(map(() => ({ bySystem: false }))),
).pipe(take(1), share()); ).pipe(
take(1),
tap(() => {
this.disconnected = new Date();
}),
share(),
);
this.receive$ = this._receive().pipe( this.receive$ = this._receive().pipe(
YGOProProtoPipe(YGOProCtos, { YGOProProtoPipe(YGOProCtos, {
onError: (error) => { onError: (error) => {
...@@ -85,7 +91,6 @@ export class Client { ...@@ -85,7 +91,6 @@ export class Client {
disconnected?: Date; disconnected?: Date;
disconnect(): undefined { disconnect(): undefined {
this.disconnected = new Date();
this.disconnectSubject.next(); this.disconnectSubject.next();
this.disconnectSubject.complete(); this.disconnectSubject.complete();
this._disconnect().then(); this._disconnect().then();
......
...@@ -30,6 +30,8 @@ export const defaultConfig = { ...@@ -30,6 +30,8 @@ export const defaultConfig = {
DECK_MAX_COPIES: '3', DECK_MAX_COPIES: '3',
OCGCORE_DEBUG_LOG: '', OCGCORE_DEBUG_LOG: '',
WELCOME: '', WELCOME: '',
NO_RECONNECT: '',
RECONNECT_TIMEOUT: '180000',
...(Object.fromEntries( ...(Object.fromEntries(
Object.entries(DefaultHostinfo).map(([key, value]) => [ Object.entries(DefaultHostinfo).map(([key, value]) => [
`HOSTINFO_${key.toUpperCase()}`, `HOSTINFO_${key.toUpperCase()}`,
......
...@@ -13,6 +13,13 @@ export const TRANSLATIONS = { ...@@ -13,6 +13,13 @@ export const TRANSLATIONS = {
watch_join: 'joined as spectator.', watch_join: 'joined as spectator.',
quit_watch: 'quited spectating', quit_watch: 'quited spectating',
left_game: 'quited game', left_game: 'quited game',
disconnect_from_game: 'disconnected from the game',
reconnect_to_game: 'reconnected to the game',
reconnect_kicked: "You are kicked out because you're logged in on other devices.",
pre_reconnecting_to_room: 'You will be reconnected to your previous game. Please pick your previous deck.',
deck_incorrect_reconnect: 'Please pick your previous deck.',
reconnect_failed: 'Reconnect failed.',
reconnecting_to_room: 'Reconnecting to server...',
}, },
'zh-CN': { 'zh-CN': {
update_required: '请更新你的客户端版本', update_required: '请更新你的客户端版本',
...@@ -27,5 +34,12 @@ export const TRANSLATIONS = { ...@@ -27,5 +34,12 @@ export const TRANSLATIONS = {
watch_join: '加入了观战', watch_join: '加入了观战',
quit_watch: '退出了观战', quit_watch: '退出了观战',
left_game: '离开了游戏', left_game: '离开了游戏',
disconnect_from_game: '断开了连接',
reconnect_to_game: '重新连接了',
reconnect_kicked: '你的账号已经在其他设备登录,你被迫下线。',
pre_reconnecting_to_room: '你有未完成的对局,即将重新连接,请选择你在本局决斗中使用的卡组并准备。',
deck_incorrect_reconnect: '请选择你在本局决斗中使用的卡组。',
reconnect_failed: '重新连接失败。',
reconnecting_to_room: '正在重新连接到服务器……',
}, },
}; };
...@@ -3,9 +3,11 @@ import { ClientVersionCheck } from './client-version-check'; ...@@ -3,9 +3,11 @@ import { ClientVersionCheck } from './client-version-check';
import { ContextState } from '../app'; import { ContextState } from '../app';
import { Welcome } from './welcome'; import { Welcome } from './welcome';
import { PlayerStatusNotify } from './player-status-notify'; import { PlayerStatusNotify } from './player-status-notify';
import { Reconnect } from './reconnect';
export const FeatsModule = createAppContext<ContextState>() export const FeatsModule = createAppContext<ContextState>()
.provide(ClientVersionCheck) .provide(ClientVersionCheck)
.provide(Welcome) .provide(Welcome)
.provide(PlayerStatusNotify) .provide(PlayerStatusNotify)
.provide(Reconnect)
.define(); .define();
This diff is collapsed.
...@@ -210,7 +210,7 @@ export class Room { ...@@ -210,7 +210,7 @@ export class Room {
} }
} }
private get joinGameMessage() { get joinGameMessage() {
return new YGOProStocJoinGame().fromPartial({ return new YGOProStocJoinGame().fromPartial({
info: { info: {
...this.hostinfo, ...this.hostinfo,
...@@ -297,9 +297,20 @@ export class Room { ...@@ -297,9 +297,20 @@ export class Room {
private sendPostWatchMessages(client: Client) { private sendPostWatchMessages(client: Client) {
client.send(new YGOProStocDuelStart()); client.send(new YGOProStocDuelStart());
// 在 SelectHand / SelectTp 阶段发送 DeckCount
// Siding 阶段不发 DeckCount
if (
this.duelStage === DuelStage.Finger ||
this.duelStage === DuelStage.FirstGo
) {
client.send(this.prepareStocDeckCount(client.pos));
}
if (this.duelStage === DuelStage.Siding) { if (this.duelStage === DuelStage.Siding) {
client.send(new YGOProStocWaitingSide()); client.send(new YGOProStocWaitingSide());
} else if (this.duelStage === DuelStage.Dueling) { } else if (this.duelStage === DuelStage.Dueling) {
// Dueling 阶段不发 DeckCount,直接发送观战消息
this.lastDuelRecord?.watchMessages.forEach((message) => { this.lastDuelRecord?.watchMessages.forEach((message) => {
client.send( client.send(
new YGOProStocGameMsg().fromPartial({ msg: message.observerView() }), new YGOProStocGameMsg().fromPartial({ msg: message.observerView() }),
...@@ -755,6 +766,7 @@ export class Room { ...@@ -755,6 +766,7 @@ export class Room {
this.allPlayers.forEach((p) => p.send(changeMsg)); this.allPlayers.forEach((p) => p.send(changeMsg));
} else if (this.duelStage === DuelStage.Siding) { } else if (this.duelStage === DuelStage.Siding) {
// In Siding stage, send DUEL_START to the player who submitted deck // In Siding stage, send DUEL_START to the player who submitted deck
// Siding 阶段不发 DeckCount
client.send(new YGOProStocDuelStart()); client.send(new YGOProStocDuelStart());
// Check if all players have submitted their decks // Check if all players have submitted their decks
...@@ -809,13 +821,54 @@ export class Room { ...@@ -809,13 +821,54 @@ export class Room {
return Promise.all(this.allPlayers.map((p) => p.sendChat(msg, type))); return Promise.all(this.allPlayers.map((p) => p.sendChat(msg, type)));
} }
firstgoPlayer?: Client; firstgoPos?: number;
private handResult = [0, 0]; handResult = [0, 0];
prepareStocDeckCount(pos: number) {
const toDeckCount = (d: YGOProDeck | undefined) => {
const res = new YGOProStocDeckCount_DeckInfo();
if (!d) {
res.main = 0;
res.extra = 0;
res.side = 0;
} else {
res.main = d.main.length;
res.extra = d.extra.length;
res.side = d.side.length;
}
return res;
};
const displayCountDecks: (YGOProDeck | undefined)[] = [0, 1].map((p) => {
const player = this.getDuelPosPlayers(p)[0];
// 优先使用 deck,如果不存在则使用 startDeck 兜底
return player?.deck || player?.startDeck;
});
// 如果是观战者或者其他特殊位置,直接按顺序显示
if (pos >= NetPlayerType.OBSERVER) {
return new YGOProStocDeckCount().fromPartial({
player0DeckCount: toDeckCount(displayCountDecks[0]),
player1DeckCount: toDeckCount(displayCountDecks[1]),
});
}
// 对于玩家,自己的卡组在前,对方的在后
const duelPos = this.getDuelPos(pos);
const selfDeck = displayCountDecks[duelPos];
const otherDeck = displayCountDecks[1 - duelPos];
return new YGOProStocDeckCount().fromPartial({
player0DeckCount: toDeckCount(selfDeck),
player1DeckCount: toDeckCount(otherDeck),
});
}
private async toFirstGo(firstgoPos: number) { private async toFirstGo(firstgoPos: number) {
this.firstgoPlayer = this.getDuelPosPlayers(firstgoPos)[0]; this.firstgoPos = firstgoPos;
this.duelStage = DuelStage.FirstGo; this.duelStage = DuelStage.FirstGo;
this.firstgoPlayer.send(new YGOProStocSelectTp()); const firstgoPlayer = this.getDuelPosPlayers(firstgoPos)[0];
firstgoPlayer.send(new YGOProStocSelectTp());
} }
private async toFinger() { private async toFinger() {
...@@ -901,36 +954,9 @@ export class Room { ...@@ -901,36 +954,9 @@ export class Room {
} }
if (this.duelRecords.length === 0) { if (this.duelRecords.length === 0) {
this.allPlayers.forEach((p) => p.send(new YGOProStocDuelStart())); this.allPlayers.forEach((p) => {
const displayCountDecks = [0, 1].map( p.send(new YGOProStocDuelStart());
(p) => this.getDuelPosPlayers(p)[0].deck!, p.send(this.prepareStocDeckCount(p.pos));
);
const toDeckCount = (d: YGOProDeck) => {
const res = new YGOProStocDeckCount_DeckInfo();
res.main = d.main.length;
res.extra = d.extra.length;
res.side = d.side.length;
return res;
};
[0, 1].forEach((p) => {
const selfDeck = displayCountDecks[p];
const otherDeck = displayCountDecks[1 - p];
this.getDuelPosPlayers(p).forEach((c) => {
c.send(
new YGOProStocDeckCount().fromPartial({
player0DeckCount: toDeckCount(selfDeck),
player1DeckCount: toDeckCount(otherDeck),
}),
);
});
});
this.watchers.forEach((c) => {
c.send(
new YGOProStocDeckCount().fromPartial({
player0DeckCount: toDeckCount(displayCountDecks[0]),
player1DeckCount: toDeckCount(displayCountDecks[1]),
}),
);
}); });
} }
...@@ -1089,7 +1115,13 @@ export class Room { ...@@ -1089,7 +1115,13 @@ export class Room {
@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) { // 检查是否是该玩家选先后手(duelPos 的第一个玩家)
const duelPos = this.getDuelPos(client);
if (duelPos !== this.firstgoPos) {
return;
}
const firstgoPlayers = this.getDuelPosPlayers(duelPos);
if (client !== firstgoPlayers[0]) {
return; return;
} }
this.isPosSwapped = this.isPosSwapped =
...@@ -1294,7 +1326,11 @@ export class Room { ...@@ -1294,7 +1326,11 @@ export class Room {
async refreshLocations( async refreshLocations(
refresh: RequireQueryLocation, refresh: RequireQueryLocation,
options: { queryFlag?: number; sendToClient?: MayBeArray<Client> } = {}, options: {
queryFlag?: number;
sendToClient?: MayBeArray<Client>;
useCache?: number;
} = {},
) { ) {
if (!this.ocgcore) { if (!this.ocgcore) {
return; return;
...@@ -1305,7 +1341,7 @@ export class Room { ...@@ -1305,7 +1341,7 @@ export class Room {
player: refresh.player, player: refresh.player,
location, location,
queryFlag: options.queryFlag ?? getZoneQueryFlag(location), queryFlag: options.queryFlag ?? getZoneQueryFlag(location),
useCache: 1, useCache: options.useCache ?? 1,
}); });
await this.dispatchGameMsg( await this.dispatchGameMsg(
new YGOProMsgUpdateData().fromPartial({ new YGOProMsgUpdateData().fromPartial({
......
import YGOProDeck from 'ygopro-deck-encode';
/**
* 比较两个卡组是否相等
* 使用 toUpdateDeckPayload 转换为 buffer 然后比较
* 这是与 srvpro 一致的比较方法
*/
export function isUpdateDeckPayloadEqual(
deck1: YGOProDeck,
deck2: YGOProDeck,
): boolean {
const uint8Array1 = deck1.toUpdateDeckPayload();
const uint8Array2 = deck2.toUpdateDeckPayload();
// 将 Uint8Array 转换为 Buffer 再比较
const buffer1 = Buffer.from(uint8Array1);
const buffer2 = Buffer.from(uint8Array2);
return buffer1.equals(buffer2);
}
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