Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
S
srvpro2
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nanahira
srvpro2
Commits
8caf4b7d
Commit
8caf4b7d
authored
Feb 14, 2026
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
reconnect feature
parent
302b5d5d
Pipeline
#43202
passed with stages
in 1 minute and 31 seconds
Changes
8
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
951 additions
and
41 deletions
+951
-41
config.example.yaml
config.example.yaml
+2
-0
src/client/client.ts
src/client/client.ts
+8
-3
src/config.ts
src/config.ts
+2
-0
src/constants/trans.ts
src/constants/trans.ts
+14
-0
src/feats/feats-module.ts
src/feats/feats-module.ts
+2
-0
src/feats/reconnect.ts
src/feats/reconnect.ts
+829
-0
src/room/room.ts
src/room/room.ts
+74
-38
src/utility/deck-compare.ts
src/utility/deck-compare.ts
+20
-0
No files found.
config.example.yaml
View file @
8caf4b7d
...
...
@@ -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"
...
...
src/client/client.ts
View file @
8caf4b7d
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
();
...
...
src/config.ts
View file @
8caf4b7d
...
...
@@ -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
()}
`
,
...
...
src/constants/trans.ts
View file @
8caf4b7d
...
...
@@ -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
:
'
正在重新连接到服务器……
'
,
},
};
src/feats/feats-module.ts
View file @
8caf4b7d
...
...
@@ -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
();
src/feats/reconnect.ts
0 → 100644
View file @
8caf4b7d
This diff is collapsed.
Click to expand it.
src/room/room.ts
View file @
8caf4b7d
...
...
@@ -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
.
firstgoP
layer
=
this
.
getDuelPosPlayers
(
firstgoPos
)[
0
]
;
this
.
firstgoP
os
=
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
({
...
...
src/utility/deck-compare.ts
0 → 100644
View file @
8caf4b7d
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
);
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment