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"
DECK_MAX_COPIES: "3"
OCGCORE_DEBUG_LOG: ""
WELCOME: ""
NO_RECONNECT: ""
RECONNECT_TIMEOUT: "180000"
HOSTINFO_LFLIST: "0"
HOSTINFO_RULE: "0"
HOSTINFO_MODE: "0"
......
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 {
YGOProCtos,
......@@ -55,7 +55,13 @@ export class Client {
.asObservable()
.pipe(map(() => ({ bySystem: true }))),
this._onDisconnect().pipe(map(() => ({ bySystem: false }))),
).pipe(take(1), share());
).pipe(
take(1),
tap(() => {
this.disconnected = new Date();
}),
share(),
);
this.receive$ = this._receive().pipe(
YGOProProtoPipe(YGOProCtos, {
onError: (error) => {
......@@ -85,7 +91,6 @@ export class Client {
disconnected?: Date;
disconnect(): undefined {
this.disconnected = new Date();
this.disconnectSubject.next();
this.disconnectSubject.complete();
this._disconnect().then();
......
......@@ -30,6 +30,8 @@ export const defaultConfig = {
DECK_MAX_COPIES: '3',
OCGCORE_DEBUG_LOG: '',
WELCOME: '',
NO_RECONNECT: '',
RECONNECT_TIMEOUT: '180000',
...(Object.fromEntries(
Object.entries(DefaultHostinfo).map(([key, value]) => [
`HOSTINFO_${key.toUpperCase()}`,
......
......@@ -13,6 +13,13 @@ export const TRANSLATIONS = {
watch_join: 'joined as spectator.',
quit_watch: 'quited spectating',
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': {
update_required: '请更新你的客户端版本',
......@@ -27,5 +34,12 @@ export const TRANSLATIONS = {
watch_join: '加入了观战',
quit_watch: '退出了观战',
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';
import { ContextState } from '../app';
import { Welcome } from './welcome';
import { PlayerStatusNotify } from './player-status-notify';
import { Reconnect } from './reconnect';
export const FeatsModule = createAppContext<ContextState>()
.provide(ClientVersionCheck)
.provide(Welcome)
.provide(PlayerStatusNotify)
.provide(Reconnect)
.define();
This diff is collapsed.
......@@ -210,7 +210,7 @@ export class Room {
}
}
private get joinGameMessage() {
get joinGameMessage() {
return new YGOProStocJoinGame().fromPartial({
info: {
...this.hostinfo,
......@@ -297,9 +297,20 @@ export class Room {
private sendPostWatchMessages(client: Client) {
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) {
client.send(new YGOProStocWaitingSide());
} else if (this.duelStage === DuelStage.Dueling) {
// Dueling 阶段不发 DeckCount,直接发送观战消息
this.lastDuelRecord?.watchMessages.forEach((message) => {
client.send(
new YGOProStocGameMsg().fromPartial({ msg: message.observerView() }),
......@@ -755,6 +766,7 @@ export class Room {
this.allPlayers.forEach((p) => p.send(changeMsg));
} else if (this.duelStage === DuelStage.Siding) {
// In Siding stage, send DUEL_START to the player who submitted deck
// Siding 阶段不发 DeckCount
client.send(new YGOProStocDuelStart());
// Check if all players have submitted their decks
......@@ -809,13 +821,54 @@ export class Room {
return Promise.all(this.allPlayers.map((p) => p.sendChat(msg, type)));
}
firstgoPlayer?: Client;
private handResult = [0, 0];
firstgoPos?: number;
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) {
this.firstgoPlayer = this.getDuelPosPlayers(firstgoPos)[0];
this.firstgoPos = firstgoPos;
this.duelStage = DuelStage.FirstGo;
this.firstgoPlayer.send(new YGOProStocSelectTp());
const firstgoPlayer = this.getDuelPosPlayers(firstgoPos)[0];
firstgoPlayer.send(new YGOProStocSelectTp());
}
private async toFinger() {
......@@ -901,36 +954,9 @@ export class Room {
}
if (this.duelRecords.length === 0) {
this.allPlayers.forEach((p) => p.send(new YGOProStocDuelStart()));
const displayCountDecks = [0, 1].map(
(p) => this.getDuelPosPlayers(p)[0].deck!,
);
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]),
}),
);
this.allPlayers.forEach((p) => {
p.send(new YGOProStocDuelStart());
p.send(this.prepareStocDeckCount(p.pos));
});
}
......@@ -1089,7 +1115,13 @@ export class Room {
@RoomMethod({ allowInDuelStages: DuelStage.FirstGo })
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;
}
this.isPosSwapped =
......@@ -1294,7 +1326,11 @@ export class Room {
async refreshLocations(
refresh: RequireQueryLocation,
options: { queryFlag?: number; sendToClient?: MayBeArray<Client> } = {},
options: {
queryFlag?: number;
sendToClient?: MayBeArray<Client>;
useCache?: number;
} = {},
) {
if (!this.ocgcore) {
return;
......@@ -1305,7 +1341,7 @@ export class Room {
player: refresh.player,
location,
queryFlag: options.queryFlag ?? getZoneQueryFlag(location),
useCache: 1,
useCache: options.useCache ?? 1,
});
await this.dispatchGameMsg(
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