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
3ac2c111
Commit
3ac2c111
authored
Feb 17, 2026
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add cloud replay
parent
b55ce242
Changes
12
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
819 additions
and
23 deletions
+819
-23
src/constants/trans.ts
src/constants/trans.ts
+23
-0
src/feats/cloud-replay/cloud-replay-service.ts
src/feats/cloud-replay/cloud-replay-service.ts
+602
-8
src/feats/cloud-replay/duel-record-player.entity.ts
src/feats/cloud-replay/duel-record-player.entity.ts
+7
-1
src/feats/cloud-replay/duel-record.entity.ts
src/feats/cloud-replay/duel-record.entity.ts
+1
-1
src/feats/cloud-replay/index.ts
src/feats/cloud-replay/index.ts
+1
-0
src/feats/cloud-replay/utility/record-codec.ts
src/feats/cloud-replay/utility/record-codec.ts
+150
-10
src/join-handlers/cloud-replay-join-handler.ts
src/join-handlers/cloud-replay-join-handler.ts
+20
-0
src/join-handlers/join-handler-module.ts
src/join-handlers/join-handler-module.ts
+2
-0
src/room/duel-record.ts
src/room/duel-record.ts
+1
-1
src/room/index.ts
src/room/index.ts
+1
-0
src/room/room-event/on-room-win.ts
src/room/room-event/on-room-win.ts
+1
-0
src/room/room.ts
src/room/room.ts
+10
-2
No files found.
src/constants/trans.ts
View file @
3ac2c111
...
...
@@ -115,6 +115,18 @@ export const TRANSLATIONS = {
koishi_ai_disabled
:
'
Windbot feature is disabled.
'
,
koishi_ai_disabled_random_room
:
'
AI is disabled in random duel rooms.
'
,
koishi_ai_room_full
:
'
Room is full, cannot add AI.
'
,
cloud_replay_no
:
'
Replay not found.
'
,
cloud_replay_error
:
'
Replay opening failed.
'
,
cloud_replay_playing
:
'
Accessing cloud replay
'
,
cloud_replay_hint
:
'
These are your recent cloud replays. Select one from the menu to continue.
'
,
cloud_replay_detail_time
:
'
Time:
'
,
cloud_replay_detail_players
:
'
Duel:
'
,
cloud_replay_detail_score
:
'
Score:
'
,
cloud_replay_detail_winner
:
'
Winner:
'
,
cloud_replay_menu_play
:
'
Play Cloud Replay
'
,
cloud_replay_menu_download_yrp
:
'
Download YRP Replay
'
,
cloud_replay_menu_back
:
'
Back
'
,
},
'
zh-CN
'
:
{
update_required
:
'
请更新你的客户端版本
'
,
...
...
@@ -224,5 +236,16 @@ export const TRANSLATIONS = {
koishi_ai_disabled
:
'
人机功能未开启。
'
,
koishi_ai_disabled_random_room
:
'
随机对战房间不允许使用 /ai。
'
,
koishi_ai_room_full
:
'
房间已满,无法添加AI。
'
,
cloud_replay_no
:
'
没有找到录像
'
,
cloud_replay_error
:
'
播放录像出错
'
,
cloud_replay_playing
:
'
正在观看云录像
'
,
cloud_replay_hint
:
'
以下是您近期的云录像,请在菜单中选择一条继续。
'
,
cloud_replay_detail_time
:
'
时间:
'
,
cloud_replay_detail_players
:
'
对局:
'
,
cloud_replay_detail_score
:
'
比分:
'
,
cloud_replay_detail_winner
:
'
胜者:
'
,
cloud_replay_menu_play
:
'
播放云录像
'
,
cloud_replay_menu_download_yrp
:
'
下载 YRP 录像
'
,
cloud_replay_menu_back
:
'
返回
'
,
},
};
src/feats/cloud-replay/cloud-replay-service.ts
View file @
3ac2c111
This diff is collapsed.
Click to expand it.
src/feats/cloud-replay/duel-record-player.entity.ts
View file @
3ac2c111
...
...
@@ -53,7 +53,7 @@ export class DuelRecordPlayer extends BaseTimeEntity {
clientKey
!
:
string
;
// getClientKey(client)
@
Column
(
'
bool
'
)
isFirst
!
:
boolean
;
//
如果 room.getIngameDuelPos(client) === 0 就是 true
isFirst
!
:
boolean
;
//
wasSwapped ? duelPos==1 : duelPos==0
@
Index
()
@
Column
(
'
smallint
'
)
...
...
@@ -71,6 +71,12 @@ export class DuelRecordPlayer extends BaseTimeEntity {
@
Column
(
'
smallint
'
)
currentDeckMainc
!
:
number
;
// client.currentDeck.main.length
@
Column
(
'
text
'
,
{})
ingameDeckBuffer
!
:
string
;
// duelRecord.players[x].deck.toPayload() base64
@
Column
(
'
smallint
'
)
ingameDeckMainc
!
:
number
;
// duelRecord.players[x].deck.main.length
@
Column
(
'
bool
'
)
winner
!
:
boolean
;
...
...
src/feats/cloud-replay/duel-record.entity.ts
View file @
3ac2c111
...
...
@@ -64,7 +64,7 @@ export class DuelRecordEntity extends BaseTimeEntity {
@
Column
({
type
:
'
text
'
,
})
responses
!
:
string
;
// duelRecord.responses
直接拼接
base64
responses
!
:
string
;
// duelRecord.responses
按 [uint8 len][payload]... 拼接再
base64
// 32 bytes binary seed => 44 chars base64.
@
Column
({
...
...
src/feats/cloud-replay/index.ts
View file @
3ac2c111
export
*
from
'
./duel-record.entity
'
;
export
*
from
'
./duel-record-player.entity
'
;
export
*
from
'
./cloud-replay-service
'
;
export
*
from
'
./utility
'
;
src/feats/cloud-replay/utility/record-codec.ts
View file @
3ac2c111
import
YGOProDeck
from
'
ygopro-deck-encode
'
;
import
{
YGOProMsgBase
,
YGOProStocGameMsg
}
from
'
ygopro-msg-encode
'
;
import
{
YGOProMsgBase
,
YGOProStoc
,
YGOProStocGameMsg
,
}
from
'
ygopro-msg-encode
'
;
import
{
Client
}
from
'
../../../client
'
;
import
{
Room
}
from
'
../../../room
'
;
const
RESPONSE_LENGTH_BYTES
=
1
;
export
function
resolvePlayerScore
(
room
:
Room
,
client
:
Client
)
{
const
duelPos
=
room
.
getIngameDuelPos
(
client
);
return
room
.
score
[
duelPos
]
||
0
;
}
function
resolveTeamOffsetBit
(
isTag
:
boolean
)
{
return
isTag
?
1
:
0
;
}
export
function
resolveIngamePosBySeat
(
pos
:
number
,
isTag
:
boolean
,
wasSwapped
:
boolean
,
)
{
if
(
!
wasSwapped
)
{
return
pos
;
}
return
pos
^
(
0x1
<<
resolveTeamOffsetBit
(
isTag
));
}
export
function
resolveRecordIngamePos
(
room
:
Room
,
client
:
Client
,
wasSwapped
:
boolean
,
)
{
return
resolveIngamePosBySeat
(
client
.
pos
,
room
.
isTag
,
wasSwapped
);
}
export
function
resolveIsFirstPlayer
(
room
:
Room
,
client
:
Client
,
wasSwapped
:
boolean
,
)
{
const
firstgoDuelPos
=
wasSwapped
?
1
:
0
;
return
room
.
getDuelPos
(
client
)
===
firstgoDuelPos
;
}
export
function
encodeMessagesBase64
(
messages
:
YGOProMsgBase
[])
{
if
(
!
messages
.
length
)
{
return
''
;
...
...
@@ -28,7 +66,13 @@ export function encodeResponsesBase64(responses: Buffer[]) {
if
(
!
responses
.
length
)
{
return
''
;
}
return
Buffer
.
concat
(
responses
).
toString
(
'
base64
'
);
const
payloads
=
responses
.
flatMap
((
response
)
=>
{
const
length
=
response
.
length
&
0xff
;
const
lengthBuffer
=
Buffer
.
alloc
(
RESPONSE_LENGTH_BYTES
,
0
);
lengthBuffer
.
writeUInt8
(
length
,
0
);
return
[
lengthBuffer
,
response
];
});
return
Buffer
.
concat
(
payloads
).
toString
(
'
base64
'
);
}
export
function
encodeSeedBase64
(
seed
:
number
[])
{
...
...
@@ -50,19 +94,115 @@ export function resolveStartDeckMainc(client: Client) {
return
client
.
startDeck
?.
main
?.
length
||
0
;
}
function
resolveCurrentDeck
(
room
:
Room
,
client
:
Client
)
{
function
resolveRecordDeck
(
room
:
Room
,
client
:
Client
,
wasSwapped
=
false
)
{
const
ingamePos
=
resolveRecordIngamePos
(
room
,
client
,
wasSwapped
);
const
duelRecordPlayer
=
room
.
lastDuelRecord
?.
players
[
ingamePos
];
return
duelRecordPlayer
?.
deck
;
}
function
resolveCurrentDeck
(
room
:
Room
,
client
:
Client
,
wasSwapped
=
false
)
{
if
(
client
.
deck
)
{
return
client
.
deck
;
}
const
ingamePos
=
room
.
getIngamePos
(
client
);
const
duelRecordPlayer
=
room
.
lastDuelRecord
?.
players
[
ingamePos
];
return
duelRecordPlayer
?.
deck
;
return
resolveRecordDeck
(
room
,
client
,
wasSwapped
);
}
export
function
resolveCurrentDeckMainc
(
room
:
Room
,
client
:
Client
,
wasSwapped
=
false
,
)
{
return
resolveCurrentDeck
(
room
,
client
,
wasSwapped
)?.
main
?.
length
||
0
;
}
export
function
encodeCurrentDeckBase64
(
room
:
Room
,
client
:
Client
,
wasSwapped
=
false
,
)
{
return
encodeDeckBase64
(
resolveCurrentDeck
(
room
,
client
,
wasSwapped
));
}
export
function
resolveCurrentDeckMainc
(
room
:
Room
,
client
:
Client
)
{
return
resolveCurrentDeck
(
room
,
client
)?.
main
?.
length
||
0
;
export
function
encodeIngameDeckBase64
(
room
:
Room
,
client
:
Client
,
wasSwapped
:
boolean
,
)
{
return
encodeDeckBase64
(
resolveRecordDeck
(
room
,
client
,
wasSwapped
));
}
export
function
encodeCurrentDeckBase64
(
room
:
Room
,
client
:
Client
)
{
return
encodeDeckBase64
(
resolveCurrentDeck
(
room
,
client
));
export
function
resolveIngameDeckMainc
(
room
:
Room
,
client
:
Client
,
wasSwapped
:
boolean
,
)
{
return
resolveRecordDeck
(
room
,
client
,
wasSwapped
)?.
main
?.
length
||
0
;
}
export
function
decodeMessagesBase64
(
messagesBase64
:
string
)
{
if
(
!
messagesBase64
)
{
return
[];
}
const
payload
=
Buffer
.
from
(
messagesBase64
,
'
base64
'
);
if
(
!
payload
.
length
)
{
return
[];
}
const
stocPackets
=
YGOProStoc
.
getInstancesFromPayload
(
payload
);
return
stocPackets
.
filter
(
(
packet
):
packet
is
YGOProStocGameMsg
=>
packet
instanceof
YGOProStocGameMsg
&&
!!
packet
.
msg
,
);
}
export
function
decodeResponsesBase64
(
responsesBase64
:
string
)
{
if
(
!
responsesBase64
)
{
return
[];
}
const
payload
=
Buffer
.
from
(
responsesBase64
,
'
base64
'
);
if
(
!
payload
.
length
)
{
return
[];
}
return
decodeLengthPrefixedResponses
(
payload
)
||
[];
}
function
decodeLengthPrefixedResponses
(
payload
:
Buffer
)
{
const
responses
:
Buffer
[]
=
[];
let
offset
=
0
;
while
(
offset
<
payload
.
length
)
{
if
(
offset
+
RESPONSE_LENGTH_BYTES
>
payload
.
length
)
{
return
undefined
;
}
const
length
=
payload
.
readUInt8
(
offset
);
offset
+=
RESPONSE_LENGTH_BYTES
;
if
(
offset
+
length
>
payload
.
length
)
{
return
undefined
;
}
responses
.
push
(
payload
.
subarray
(
offset
,
offset
+
length
));
offset
+=
length
;
}
return
responses
;
}
export
function
decodeSeedBase64
(
seedBase64
:
string
)
{
const
decoded
=
seedBase64
?
Buffer
.
from
(
seedBase64
,
'
base64
'
)
:
Buffer
.
alloc
(
0
);
const
raw
=
Buffer
.
alloc
(
32
,
0
);
decoded
.
copy
(
raw
,
0
,
0
,
Math
.
min
(
decoded
.
length
,
raw
.
length
));
const
seed
:
number
[]
=
[];
for
(
let
i
=
0
;
i
<
8
;
i
+=
1
)
{
seed
.
push
(
raw
.
readUInt32LE
(
i
*
4
)
>>>
0
);
}
return
seed
;
}
export
function
decodeDeckBase64
(
deckBase64
:
string
,
mainc
:
number
)
{
if
(
!
deckBase64
)
{
return
new
YGOProDeck
();
}
const
payload
=
Buffer
.
from
(
deckBase64
,
'
base64
'
);
if
(
!
payload
.
length
)
{
return
new
YGOProDeck
();
}
return
YGOProDeck
.
fromUpdateDeckPayload
(
payload
,
(
_code
,
index
)
=>
{
return
index
>=
mainc
;
});
}
src/join-handlers/cloud-replay-join-handler.ts
0 → 100644
View file @
3ac2c111
import
{
YGOProCtosJoinGame
}
from
'
ygopro-msg-encode
'
;
import
{
Context
}
from
'
../app
'
;
import
{
CloudReplayService
}
from
'
../feats
'
;
export
class
CloudReplayJoinHandler
{
private
cloudReplayService
=
this
.
ctx
.
get
(()
=>
CloudReplayService
);
constructor
(
private
ctx
:
Context
)
{
this
.
ctx
.
middleware
(
YGOProCtosJoinGame
,
async
(
msg
,
client
,
next
)
=>
{
const
pass
=
(
msg
.
pass
||
''
).
trim
();
if
(
!
pass
)
{
return
next
();
}
if
(
await
this
.
cloudReplayService
.
tryHandleJoinPass
(
pass
,
client
))
{
return
;
}
return
next
();
});
}
}
src/join-handlers/join-handler-module.ts
View file @
3ac2c111
...
...
@@ -4,6 +4,7 @@ import { ClientVersionCheck } from '../feats';
import
{
JoinWindbotAi
,
JoinWindbotToken
}
from
'
../feats/windbot
'
;
import
{
JoinRoom
}
from
'
./join-room
'
;
import
{
JoinRoomIp
}
from
'
./join-room-ip
'
;
import
{
CloudReplayJoinHandler
}
from
'
./cloud-replay-join-handler
'
;
import
{
JoinFallback
}
from
'
./fallback
'
;
import
{
JoinPrechecks
}
from
'
./join-prechecks
'
;
import
{
RandomDuelJoinHandler
}
from
'
./random-duel-join-handler
'
;
...
...
@@ -20,6 +21,7 @@ export const JoinHandlerModule = createAppContext<ContextState>()
.
provide
(
RandomDuelJoinHandler
)
.
provide
(
JoinWindbotAi
)
.
provide
(
JoinRoomIp
)
.
provide
(
CloudReplayJoinHandler
)
.
provide
(
JoinRoom
)
.
provide
(
JoinBlankPassMenu
)
.
provide
(
JoinBlankPassRandomDuel
)
...
...
src/room/duel-record.ts
View file @
3ac2c111
...
...
@@ -33,7 +33,7 @@ export class DuelRecord {
});
}
toYrp
(
room
:
Room
)
{
toYrp
(
room
:
Pick
<
Room
,
'
hostinfo
'
|
'
isTag
'
>
)
{
const
isTag
=
room
.
isTag
;
// Create replay header
...
...
src/room/index.ts
View file @
3ac2c111
export
*
from
'
./room
'
;
export
*
from
'
./duel-record
'
;
export
*
from
'
./room-manager
'
;
export
*
from
'
./duel-stage
'
;
export
*
from
'
./room-event/on-room-create
'
;
...
...
src/room/room-event/on-room-win.ts
View file @
3ac2c111
...
...
@@ -7,6 +7,7 @@ export class OnRoomWin extends RoomEvent {
room
:
Room
,
public
winMsg
:
YGOProMsgWin
,
public
winMatch
=
false
,
public
wasSwapped
=
false
,
)
{
super
(
room
);
}
...
...
src/room/room.ts
View file @
3ac2c111
...
...
@@ -485,6 +485,7 @@ export class Room {
this
.
resetResponseState
();
this
.
disposeOcgcore
();
this
.
ocgcore
=
undefined
;
const
wasSwapped
=
this
.
isPosSwapped
;
if
(
this
.
duelStage
===
DuelStage
.
Siding
)
{
await
Promise
.
all
(
this
.
playingPlayers
...
...
@@ -525,7 +526,7 @@ export class Room {
await
this
.
changeSide
();
}
await
this
.
ctx
.
dispatch
(
new
OnRoomWin
(
this
,
exactWinMsg
,
winMatch
),
new
OnRoomWin
(
this
,
exactWinMsg
,
winMatch
,
wasSwapped
),
this
.
getDuelPosPlayers
(
duelPos
)[
0
],
);
if
(
winMatch
)
{
...
...
@@ -869,7 +870,14 @@ export class Room {
const
changeMsg
=
client
.
prepareChangePacket
();
this
.
allPlayers
.
forEach
((
p
)
=>
p
.
send
(
changeMsg
));
if
(
this
.
noHost
)
{
const
allReadyAndFull
=
this
.
players
.
every
((
player
)
=>
!!
player
?.
deck
);
let
allReadyAndFull
=
true
;
for
(
let
i
=
0
;
i
<
this
.
players
.
length
;
i
++
)
{
const
p
=
this
.
players
[
i
];
if
(
!
p
||
!
p
.
deck
)
{
allReadyAndFull
=
false
;
break
;
}
}
if
(
allReadyAndFull
)
{
await
this
.
startGame
();
}
...
...
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