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
aff3079d
Commit
aff3079d
authored
Feb 16, 2026
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add side timeout
parent
9e569115
Pipeline
#43251
passed with stages
in 2 minutes and 1 second
Changes
13
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
235 additions
and
3 deletions
+235
-3
AGENTS.md
AGENTS.md
+2
-1
config.example.yaml
config.example.yaml
+2
-1
src/app.ts
src/app.ts
+2
-0
src/config.ts
src/config.ts
+3
-0
src/constants/trans.ts
src/constants/trans.ts
+12
-0
src/feats/feats-module.ts
src/feats/feats-module.ts
+2
-0
src/feats/side-timeout.ts
src/feats/side-timeout.ts
+166
-0
src/room/index.ts
src/room/index.ts
+6
-0
src/room/room-event/on-room-siding-ready.ts
src/room/room-event/on-room-siding-ready.ts
+3
-0
src/room/room-event/on-room-siding-start.ts
src/room/room-event/on-room-siding-start.ts
+3
-0
src/room/room.ts
src/room/room.ts
+4
-0
src/services/middleware-rx.ts
src/services/middleware-rx.ts
+29
-0
src/windbot/reverse-ws-client.ts
src/windbot/reverse-ws-client.ts
+1
-1
No files found.
AGENTS.md
View file @
aff3079d
...
@@ -4,7 +4,8 @@
...
@@ -4,7 +4,8 @@
## 项目规范
## 项目规范
-
非必要不要在 Room 和 Client 里面加字段或者方法。如果可以的话请使用定义 interface 进行依赖合并。
-
禁止在模块里面保存
`Client`
或者
`Room`
的强引用(包括
`Map`
key/value、闭包长期持有等)。如需关联状态,优先使用
`pos`
、
`room.name`
等轻量标识。
-
禁止直接在
`Client`
和
`Room`
类里面添加耦合业务模块的字段或者方法。需要扩展时可通过定义 interface 做依赖合并;若扩展房间流程,允许在
`Room`
里新增并
`dispatch`
专用事件。
-
进行协议设计需要核对 ygopro 和 srvpro 的 coffee 和 cpp 的实现。
-
进行协议设计需要核对 ygopro 和 srvpro 的 coffee 和 cpp 的实现。
-
尽量定义新的模块实现功能,而不是在之前的方法上进行修改。
-
尽量定义新的模块实现功能,而不是在之前的方法上进行修改。
-
配置在 config.ts 里面写默认类型。注意所有类型必须是 string 并且全部大写字母。改了之后需要 npm run gen:config-example 生成 config.example.yaml
-
配置在 config.ts 里面写默认类型。注意所有类型必须是 string 并且全部大写字母。改了之后需要 npm run gen:config-example 生成 config.example.yaml
...
...
config.example.yaml
View file @
aff3079d
...
@@ -26,13 +26,14 @@ deckMaxCopies: 3
...
@@ -26,13 +26,14 @@ deckMaxCopies: 3
ocgcoreDebugLog
:
0
ocgcoreDebugLog
:
0
ocgcoreWasmPath
:
"
"
ocgcoreWasmPath
:
"
"
welcome
:
"
"
welcome
:
"
"
enableWindbot
:
1
enableWindbot
:
0
windbotBotlist
:
./windbot/bots.json
windbotBotlist
:
./windbot/bots.json
windbotSpawn
:
0
windbotSpawn
:
0
windbotEndpoint
:
http://127.0.0.1:2399
windbotEndpoint
:
http://127.0.0.1:2399
windbotMyIp
:
127.0.0.1
windbotMyIp
:
127.0.0.1
enableReconnect
:
1
enableReconnect
:
1
reconnectTimeout
:
180000
reconnectTimeout
:
180000
sideTimeoutMinutes
:
3
hostinfoLflist
:
0
hostinfoLflist
:
0
hostinfoRule
:
0
hostinfoRule
:
0
hostinfoMode
:
0
hostinfoMode
:
0
...
...
src/app.ts
View file @
aff3079d
...
@@ -9,6 +9,7 @@ import { JoinHandlerModule } from './join-handlers/join-handler-module';
...
@@ -9,6 +9,7 @@ import { JoinHandlerModule } from './join-handlers/join-handler-module';
import
{
RoomModule
}
from
'
./room/room-module
'
;
import
{
RoomModule
}
from
'
./room/room-module
'
;
import
{
SqljsFactory
,
SqljsLoader
}
from
'
./services/sqljs
'
;
import
{
SqljsFactory
,
SqljsLoader
}
from
'
./services/sqljs
'
;
import
{
FeatsModule
}
from
'
./feats/feats-module
'
;
import
{
FeatsModule
}
from
'
./feats/feats-module
'
;
import
{
MiddlewareRx
}
from
'
./services/middleware-rx
'
;
const
core
=
createAppContext
()
const
core
=
createAppContext
()
.
provide
(
ConfigService
,
{
.
provide
(
ConfigService
,
{
...
@@ -16,6 +17,7 @@ const core = createAppContext()
...
@@ -16,6 +17,7 @@ const core = createAppContext()
})
})
.
provide
(
Logger
,
{
merge
:
[
'
createLogger
'
]
})
.
provide
(
Logger
,
{
merge
:
[
'
createLogger
'
]
})
.
provide
(
Emitter
,
{
merge
:
[
'
dispatch
'
,
'
middleware
'
,
'
removeMiddleware
'
]
})
.
provide
(
Emitter
,
{
merge
:
[
'
dispatch
'
,
'
middleware
'
,
'
removeMiddleware
'
]
})
.
provide
(
MiddlewareRx
,
{
merge
:
[
'
event$
'
]
})
.
provide
(
HttpClient
,
{
merge
:
[
'
http
'
]
})
.
provide
(
HttpClient
,
{
merge
:
[
'
http
'
]
})
.
provide
(
AragamiService
,
{
merge
:
[
'
aragami
'
]
})
.
provide
(
AragamiService
,
{
merge
:
[
'
aragami
'
]
})
.
provide
(
SqljsLoader
,
{
.
provide
(
SqljsLoader
,
{
...
...
src/config.ts
View file @
aff3079d
...
@@ -79,6 +79,9 @@ export const defaultConfig = {
...
@@ -79,6 +79,9 @@ export const defaultConfig = {
ENABLE_RECONNECT
:
'
1
'
,
ENABLE_RECONNECT
:
'
1
'
,
// Reconnect timeout after disconnect. Format: integer string in milliseconds (ms).
// Reconnect timeout after disconnect. Format: integer string in milliseconds (ms).
RECONNECT_TIMEOUT
:
'
180000
'
,
RECONNECT_TIMEOUT
:
'
180000
'
,
// Side deck timeout in minutes during siding stage.
// Format: integer string. '0' or negative disables the feature.
SIDE_TIMEOUT_MINUTES
:
'
3
'
,
// Room hostinfo defaults expanded into HOSTINFO_* keys.
// Room hostinfo defaults expanded into HOSTINFO_* keys.
// Format: each HOSTINFO_* value is a string; numeric fields use integer strings.
// Format: each HOSTINFO_* value is a string; numeric fields use integer strings.
// Unit note: HOSTINFO_TIME_LIMIT is in seconds (s).
// Unit note: HOSTINFO_TIME_LIMIT is in seconds (s).
...
...
src/constants/trans.ts
View file @
aff3079d
...
@@ -29,6 +29,12 @@ export const TRANSLATIONS = {
...
@@ -29,6 +29,12 @@ export const TRANSLATIONS = {
deck_incorrect_reconnect
:
'
Please pick your previous deck.
'
,
deck_incorrect_reconnect
:
'
Please pick your previous deck.
'
,
reconnect_failed
:
'
Reconnect failed.
'
,
reconnect_failed
:
'
Reconnect failed.
'
,
reconnecting_to_room
:
'
Reconnecting to server...
'
,
reconnecting_to_room
:
'
Reconnecting to server...
'
,
side_timeout_part1
:
'
Changing side time is limited to
'
,
side_timeout_part2
:
'
minutes.
'
,
side_remain_part1
:
'
Remaining side changing time:
'
,
side_remain_part2
:
'
minutes.
'
,
side_overtime
:
'
You exceeded side changing time and were kicked by system.
'
,
side_overtime_room
:
'
exceeded side changing time and was kicked by system.
'
,
},
},
'
zh-CN
'
:
{
'
zh-CN
'
:
{
update_required
:
'
请更新你的客户端版本
'
,
update_required
:
'
请更新你的客户端版本
'
,
...
@@ -57,5 +63,11 @@ export const TRANSLATIONS = {
...
@@ -57,5 +63,11 @@ export const TRANSLATIONS = {
deck_incorrect_reconnect
:
'
请选择你在本局决斗中使用的卡组。
'
,
deck_incorrect_reconnect
:
'
请选择你在本局决斗中使用的卡组。
'
,
reconnect_failed
:
'
重新连接失败。
'
,
reconnect_failed
:
'
重新连接失败。
'
,
reconnecting_to_room
:
'
正在重新连接到服务器……
'
,
reconnecting_to_room
:
'
正在重新连接到服务器……
'
,
side_timeout_part1
:
'
你现在有
'
,
side_timeout_part2
:
'
分钟来更换副卡组。
'
,
side_remain_part1
:
'
更换副卡组剩余时间:
'
,
side_remain_part2
:
'
分钟。
'
,
side_overtime
:
'
你更换副卡组超时,已被系统踢出。
'
,
side_overtime_room
:
'
更换副卡组超时,已被系统踢出。
'
,
},
},
};
};
src/feats/feats-module.ts
View file @
aff3079d
...
@@ -5,6 +5,7 @@ import { Welcome } from './welcome';
...
@@ -5,6 +5,7 @@ import { Welcome } from './welcome';
import
{
PlayerStatusNotify
}
from
'
./player-status-notify
'
;
import
{
PlayerStatusNotify
}
from
'
./player-status-notify
'
;
import
{
Reconnect
}
from
'
./reconnect
'
;
import
{
Reconnect
}
from
'
./reconnect
'
;
import
{
WindbotModule
}
from
'
../windbot
'
;
import
{
WindbotModule
}
from
'
../windbot
'
;
import
{
SideTimeout
}
from
'
./side-timeout
'
;
export
const
FeatsModule
=
createAppContext
<
ContextState
>
()
export
const
FeatsModule
=
createAppContext
<
ContextState
>
()
.
provide
(
ClientVersionCheck
)
.
provide
(
ClientVersionCheck
)
...
@@ -12,4 +13,5 @@ export const FeatsModule = createAppContext<ContextState>()
...
@@ -12,4 +13,5 @@ export const FeatsModule = createAppContext<ContextState>()
.
provide
(
Welcome
)
.
provide
(
Welcome
)
.
provide
(
PlayerStatusNotify
)
.
provide
(
PlayerStatusNotify
)
.
provide
(
Reconnect
)
.
provide
(
Reconnect
)
.
provide
(
SideTimeout
)
.
define
();
.
define
();
src/feats/side-timeout.ts
0 → 100644
View file @
aff3079d
import
{
ChatColor
}
from
'
ygopro-msg-encode
'
;
import
{
Context
}
from
'
../app
'
;
import
{
DuelStage
,
OnRoomFinalize
,
OnRoomGameStart
,
OnRoomLeavePlayer
,
OnRoomSidingReady
,
OnRoomSidingStart
,
Room
,
}
from
'
../room
'
;
import
{
merge
,
Subscription
,
timer
}
from
'
rxjs
'
;
import
{
filter
,
finalize
,
share
,
take
,
takeUntil
}
from
'
rxjs/operators
'
;
declare
module
'
../room
'
{
interface
Room
{
sideTimeoutSubscriptions
?:
Map
<
number
,
Subscription
>
;
sideTimeoutRemainMinutes
?:
Map
<
number
,
number
>
;
}
}
export
class
SideTimeout
{
private
logger
=
this
.
ctx
.
createLogger
(
'
SideTimeout
'
);
private
sideTimeoutMinutes
=
this
.
ctx
.
config
.
getInt
(
'
SIDE_TIMEOUT_MINUTES
'
);
private
onSidingReady$
=
this
.
ctx
.
event$
(
OnRoomSidingReady
).
pipe
(
share
());
private
onLeavePlayer$
=
this
.
ctx
.
event$
(
OnRoomLeavePlayer
).
pipe
(
share
());
private
onGameStart$
=
this
.
ctx
.
event$
(
OnRoomGameStart
).
pipe
(
share
());
private
onFinalize$
=
this
.
ctx
.
event$
(
OnRoomFinalize
).
pipe
(
share
());
constructor
(
private
ctx
:
Context
)
{
if
(
this
.
sideTimeoutMinutes
<=
0
)
{
return
;
}
this
.
ctx
.
event$
(
OnRoomSidingStart
).
subscribe
(({
msg
})
=>
{
void
this
.
handleSidingStart
(
msg
.
room
).
catch
((
error
)
=>
{
this
.
logger
.
warn
({
error
},
'
Failed to start side timeout
'
);
});
});
}
private
async
handleSidingStart
(
room
:
Room
)
{
if
(
room
.
duelStage
!==
DuelStage
.
Siding
)
{
return
;
}
await
Promise
.
all
(
room
.
playingPlayers
.
map
(
async
(
player
)
=>
{
await
this
.
startSideTimeout
(
room
,
player
.
pos
);
}),
);
}
private
getSubscriptions
(
room
:
Room
):
Map
<
number
,
Subscription
>
{
if
(
!
room
.
sideTimeoutSubscriptions
)
{
room
.
sideTimeoutSubscriptions
=
new
Map
();
}
return
room
.
sideTimeoutSubscriptions
;
}
private
getRemainMinutes
(
room
:
Room
):
Map
<
number
,
number
>
{
if
(
!
room
.
sideTimeoutRemainMinutes
)
{
room
.
sideTimeoutRemainMinutes
=
new
Map
();
}
return
room
.
sideTimeoutRemainMinutes
;
}
private
clearSideTimeout
(
room
:
Room
,
pos
:
number
)
{
const
subscriptions
=
this
.
getSubscriptions
(
room
);
const
subscription
=
subscriptions
.
get
(
pos
);
if
(
subscription
)
{
subscription
.
unsubscribe
();
}
subscriptions
.
delete
(
pos
);
this
.
getRemainMinutes
(
room
).
delete
(
pos
);
}
private
createStopSignal
(
room
:
Room
,
pos
:
number
)
{
return
merge
(
this
.
onSidingReady$
.
pipe
(
filter
((
event
)
=>
event
.
msg
.
room
===
room
&&
event
.
client
.
pos
===
pos
),
),
this
.
onLeavePlayer$
.
pipe
(
filter
((
event
)
=>
event
.
msg
.
room
===
room
&&
event
.
msg
.
oldPos
===
pos
),
),
this
.
onGameStart$
.
pipe
(
filter
((
event
)
=>
event
.
msg
.
room
===
room
)),
this
.
onFinalize$
.
pipe
(
filter
((
event
)
=>
event
.
msg
.
room
===
room
)),
).
pipe
(
take
(
1
));
}
private
async
startSideTimeout
(
room
:
Room
,
pos
:
number
)
{
const
client
=
room
.
players
[
pos
];
if
(
!
client
)
{
return
;
}
this
.
clearSideTimeout
(
room
,
pos
);
this
.
getRemainMinutes
(
room
).
set
(
pos
,
this
.
sideTimeoutMinutes
);
await
client
.
sendChat
(
`#{side_timeout_part1}
${
this
.
sideTimeoutMinutes
}
#{side_timeout_part2}`
,
ChatColor
.
BABYBLUE
,
);
const
stopSignal
=
this
.
createStopSignal
(
room
,
pos
);
const
subscription
=
timer
(
60
_000
,
60
_000
)
.
pipe
(
takeUntil
(
stopSignal
),
finalize
(()
=>
{
const
subscriptions
=
this
.
getSubscriptions
(
room
);
if
(
subscriptions
.
get
(
pos
)
===
subscription
)
{
subscriptions
.
delete
(
pos
);
}
this
.
getRemainMinutes
(
room
).
delete
(
pos
);
}),
)
.
subscribe
(()
=>
{
void
this
.
tickSideTimeout
(
room
,
pos
).
catch
((
error
)
=>
{
this
.
logger
.
warn
({
error
},
'
Failed to process side timeout tick
'
);
});
});
this
.
getSubscriptions
(
room
).
set
(
pos
,
subscription
);
}
private
async
tickSideTimeout
(
room
:
Room
,
pos
:
number
)
{
if
(
room
.
finalizing
||
room
.
duelStage
!==
DuelStage
.
Siding
)
{
this
.
clearSideTimeout
(
room
,
pos
);
return
;
}
const
remainMap
=
this
.
getRemainMinutes
(
room
);
const
remainMinutes
=
remainMap
.
get
(
pos
);
if
(
!
remainMinutes
)
{
this
.
clearSideTimeout
(
room
,
pos
);
return
;
}
const
client
=
room
.
players
[
pos
];
if
(
!
client
||
client
.
roomName
!==
room
.
name
||
client
.
disconnected
||
client
.
pos
!==
pos
)
{
this
.
clearSideTimeout
(
room
,
pos
);
return
;
}
if
(
remainMinutes
<=
1
)
{
this
.
clearSideTimeout
(
room
,
pos
);
await
room
.
sendChat
(
`
${
client
.
name
}
#{side_overtime_room}`
,
ChatColor
.
BABYBLUE
,
);
await
client
.
sendChat
(
'
#{side_overtime}
'
,
ChatColor
.
RED
);
client
.
disconnect
();
return
;
}
const
nextRemainMinutes
=
remainMinutes
-
1
;
remainMap
.
set
(
pos
,
nextRemainMinutes
);
await
client
.
sendChat
(
`#{side_remain_part1}
${
nextRemainMinutes
}
#{side_remain_part2}`
,
ChatColor
.
BABYBLUE
,
);
}
}
src/room/index.ts
View file @
aff3079d
export
*
from
'
./room
'
;
export
*
from
'
./room
'
;
export
*
from
'
./room-manager
'
;
export
*
from
'
./room-manager
'
;
export
*
from
'
./duel-stage
'
;
export
*
from
'
./room-event/on-room-finalize
'
;
export
*
from
'
./room-event/on-room-finalize
'
;
export
*
from
'
./room-event/on-room-game-start
'
;
export
*
from
'
./room-event/on-room-leave-player
'
;
export
*
from
'
./room-event/on-room-siding-ready
'
;
export
*
from
'
./room-event/on-room-siding-start
'
;
export
*
from
'
./room-event/on-room-win
'
;
export
*
from
'
./default-hostinfo-provder
'
;
export
*
from
'
./default-hostinfo-provder
'
;
src/room/room-event/on-room-siding-ready.ts
0 → 100644
View file @
aff3079d
import
{
RoomEvent
}
from
'
./room-event
'
;
export
class
OnRoomSidingReady
extends
RoomEvent
{}
src/room/room-event/on-room-siding-start.ts
0 → 100644
View file @
aff3079d
import
{
RoomEvent
}
from
'
./room-event
'
;
export
class
OnRoomSidingStart
extends
RoomEvent
{}
src/room/room.ts
View file @
aff3079d
...
@@ -94,6 +94,8 @@ import { makeArray } from 'aragami/dist/src/utility/utility';
...
@@ -94,6 +94,8 @@ import { makeArray } from 'aragami/dist/src/utility/utility';
import
path
from
'
path
'
;
import
path
from
'
path
'
;
import
{
OnRoomCreate
}
from
'
./room-event/on-room-create
'
;
import
{
OnRoomCreate
}
from
'
./room-event/on-room-create
'
;
import
{
OnRoomFinalize
}
from
'
./room-event/on-room-finalize
'
;
import
{
OnRoomFinalize
}
from
'
./room-event/on-room-finalize
'
;
import
{
OnRoomSidingStart
}
from
'
./room-event/on-room-siding-start
'
;
import
{
OnRoomSidingReady
}
from
'
./room-event/on-room-siding-ready
'
;
const
{
OcgcoreScriptConstants
}
=
_OcgcoreConstants
;
const
{
OcgcoreScriptConstants
}
=
_OcgcoreConstants
;
...
@@ -435,6 +437,7 @@ export class Room {
...
@@ -435,6 +437,7 @@ export class Room {
for
(
const
p
of
this
.
watchers
)
{
for
(
const
p
of
this
.
watchers
)
{
p
.
send
(
new
YGOProStocWaitingSide
());
p
.
send
(
new
YGOProStocWaitingSide
());
}
}
await
this
.
ctx
.
dispatch
(
new
OnRoomSidingStart
(
this
),
this
.
playingPlayers
[
0
]);
}
}
get
lastDuelRecord
()
{
get
lastDuelRecord
()
{
...
@@ -806,6 +809,7 @@ export class Room {
...
@@ -806,6 +809,7 @@ export class Room {
// 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
// Siding 阶段不发 DeckCount
client
.
send
(
new
YGOProStocDuelStart
());
client
.
send
(
new
YGOProStocDuelStart
());
await
this
.
ctx
.
dispatch
(
new
OnRoomSidingReady
(
this
),
client
);
// Check if all players have submitted their decks
// Check if all players have submitted their decks
const
allReady
=
this
.
playingPlayers
.
every
((
p
)
=>
p
.
deck
);
const
allReady
=
this
.
playingPlayers
.
every
((
p
)
=>
p
.
deck
);
...
...
src/services/middleware-rx.ts
0 → 100644
View file @
aff3079d
import
{
AppContext
,
ClassType
,
Middleware
,
ProtoMiddlewareFunc
}
from
'
nfkit
'
;
import
{
Emitter
}
from
'
./emitter
'
;
import
{
Observable
}
from
'
rxjs
'
;
import
{
Client
}
from
'
../client
'
;
export
class
MiddlewareRx
{
constructor
(
private
ctx
:
AppContext
)
{}
private
emitter
=
this
.
ctx
.
get
(()
=>
Emitter
);
event$
<
T
>
(
cls
:
ClassType
<
T
>
,
prior
=
false
)
{
return
new
Observable
<
{
msg
:
T
;
client
:
Client
;
}
>
((
sub
)
=>
{
const
handler
:
Middleware
<
ProtoMiddlewareFunc
<
[
client
:
Client
],
T
>>
=
(
msg
,
client
,
next
,
)
=>
{
sub
.
next
({
msg
,
client
});
return
next
();
};
this
.
emitter
.
middleware
(
cls
,
handler
,
prior
);
return
()
=>
{
this
.
emitter
.
removeMiddleware
(
cls
,
handler
);
}
});
}
}
src/windbot/reverse-ws-client.ts
View file @
aff3079d
...
@@ -68,7 +68,7 @@ export class ReverseWsClient extends Client {
...
@@ -68,7 +68,7 @@ export class ReverseWsClient extends Client {
}
}
physicalIp
():
string
{
physicalIp
():
string
{
return
this
.
endpointI
p
;
return
this
.
i
p
;
}
}
xffIp
():
string
|
undefined
{
xffIp
():
string
|
undefined
{
...
...
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