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
3ca2aca1
Commit
3ca2aca1
authored
Feb 18, 2026
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add challonge
parent
3ec356e0
Pipeline
#43300
passed with stages
in 1 minute and 33 seconds
Changes
19
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
1049 additions
and
45 deletions
+1049
-45
config.example.yaml
config.example.yaml
+10
-1
src/config.ts
src/config.ts
+21
-0
src/constants/trans.ts
src/constants/trans.ts
+26
-2
src/feats/challonge-api.ts
src/feats/challonge-api.ts
+186
-0
src/feats/challonge-service.ts
src/feats/challonge-service.ts
+588
-0
src/feats/cloud-replay/cloud-replay-service.ts
src/feats/cloud-replay/cloud-replay-service.ts
+11
-0
src/feats/feats-module.ts
src/feats/feats-module.ts
+2
-0
src/feats/index.ts
src/feats/index.ts
+2
-0
src/feats/random-duel/provider.ts
src/feats/random-duel/provider.ts
+0
-6
src/feats/windbot/join-windbot-ai.ts
src/feats/windbot/join-windbot-ai.ts
+0
-1
src/join-handlers/challonge-join-handler.ts
src/join-handlers/challonge-join-handler.ts
+85
-0
src/join-handlers/join-handler-module.ts
src/join-handlers/join-handler-module.ts
+10
-8
src/legacy-api/legacy-api-deck-service.ts
src/legacy-api/legacy-api-deck-service.ts
+75
-4
src/legacy-api/legacy-api-service.ts
src/legacy-api/legacy-api-service.ts
+6
-2
src/legacy-api/legacy-ban-service.ts
src/legacy-api/legacy-ban-service.ts
+3
-1
src/legacy-api/utility/deck-name-match.ts
src/legacy-api/utility/deck-name-match.ts
+1
-16
src/room/room.ts
src/room/room.ts
+7
-3
src/services/typeorm.ts
src/services/typeorm.ts
+0
-1
src/utility/deck-name-match.ts
src/utility/deck-name-match.ts
+16
-0
No files found.
config.example.yaml
View file @
3ca2aca1
...
...
@@ -10,7 +10,8 @@ dbName: srvpro2
dbNoInit
:
0
redisUrl
:
"
"
logLevel
:
info
wsPort
:
0
wsHost
:
"
"
wsPort
:
7912
enableSsl
:
0
sslPath
:
./ssl
sslCert
:
"
"
...
...
@@ -66,6 +67,14 @@ enableReconnect: 1
enableCloudReplay
:
1
tournamentMode
:
0
tournamentModeCheckDeck
:
1
challongeEnabled
:
0
challongeNoMatchMode
:
0
challongePostDetailedScore
:
1
challongePostScoreMidduel
:
1
challongeCacheTtl
:
60000
challongeApiKey
:
"
"
challongeTournamentId
:
"
"
challongeUrl
:
https://api.challonge.com
blockReplayToPlayer
:
0
enableRoomlist
:
1
reconnectTimeout
:
180000
...
...
src/config.ts
View file @
3ca2aca1
...
...
@@ -154,6 +154,27 @@ export const defaultConfig = {
// Enable tournament mode deck lock check hook.
// Boolean parse rule (default true): only '0'/'false'/'null' => false, otherwise true.
TOURNAMENT_MODE_CHECK_DECK
:
'
1
'
,
// Enable Challonge integration.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
CHALLONGE_ENABLED
:
'
0
'
,
// Disable challonge room name "M#" prefix and use pure match id as room name.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
CHALLONGE_NO_MATCH_MODE
:
'
0
'
,
// Post detailed match score to Challonge (for example 2-1).
// Boolean parse rule (default true): only '0'/'false'/'null' => false, otherwise true.
CHALLONGE_POST_DETAILED_SCORE
:
'
1
'
,
// Post score at siding stage without winner_id (midduel sync).
// Boolean parse rule (default true): only '0'/'false'/'null' => false, otherwise true.
CHALLONGE_POST_SCORE_MIDDUEL
:
'
1
'
,
// Challonge tournament cache TTL in milliseconds.
// Format: integer string in milliseconds (ms).
CHALLONGE_CACHE_TTL
:
'
60000
'
,
// Challonge API key.
CHALLONGE_API_KEY
:
''
,
// Challonge tournament id/slug.
CHALLONGE_TOURNAMENT_ID
:
''
,
// Challonge API base URL.
CHALLONGE_URL
:
'
https://api.challonge.com
'
,
// Block replay packets to players who are currently in a room.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
BLOCK_REPLAY_TO_PLAYER
:
'
0
'
,
...
...
src/constants/trans.ts
View file @
3ca2aca1
...
...
@@ -18,6 +18,7 @@ export const TRANSLATIONS = {
replay_hint_part1
:
'
Sending the replay of the duel number
'
,
replay_hint_part2
:
'
.
'
,
watch_join
:
'
joined as spectator.
'
,
cannot_to_observer
:
'
Spectating is not allowed in a matchup.
'
,
quit_watch
:
'
quited spectating
'
,
left_game
:
'
quited game
'
,
disconnect_from_game
:
'
disconnected from the game
'
,
...
...
@@ -130,6 +131,8 @@ export const TRANSLATIONS = {
cloud_replay_playing
:
'
Accessing cloud replay
'
,
cloud_replay_hint
:
'
These are your recent cloud replays. Select one from the menu to continue.
'
,
cloud_replay_delay_part1
:
'
The replay code for last duel is
'
,
cloud_replay_delay_part2
:
'
. It can be accessed after this match.
'
,
cloud_replay_detail_time
:
'
Time:
'
,
cloud_replay_detail_players
:
'
Duel:
'
,
cloud_replay_detail_score
:
'
Score:
'
,
...
...
@@ -139,6 +142,15 @@ export const TRANSLATIONS = {
cloud_replay_menu_play
:
'
Play Cloud Replay
'
,
cloud_replay_menu_download_yrp
:
'
Download YRP Replay
'
,
cloud_replay_menu_back
:
'
Back
'
,
challonge_user_not_found
:
'
You are not a participant of the tournament.
'
,
challonge_match_load_failed
:
'
Failed loading tournament info.
'
,
challonge_match_not_found
:
'
Your current match was not found.
'
,
challonge_match_already_finished
:
'
Your current match was already finished. Please call the judge for any help.
'
,
challonge_match_created
:
'
A room for match only is created. Your opponent will join in automatically.
'
,
challonge_player_already_in
:
'
Please do not enter the room you are already in.
'
,
death_cancel
:
'
Over-match has been canceled from this game.
'
,
death_start
:
'
Over-time match has begun, player with higher LP will win the single game after 4 turns.
'
,
...
...
@@ -184,6 +196,7 @@ export const TRANSLATIONS = {
replay_hint_part1
:
'
正在发送第
'
,
replay_hint_part2
:
'
局决斗的录像。
'
,
watch_join
:
'
加入了观战
'
,
cannot_to_observer
:
'
匹配模式中决斗者不允许观战。
'
,
quit_watch
:
'
退出了观战
'
,
left_game
:
'
离开了游戏
'
,
disconnect_from_game
:
'
断开了连接
'
,
...
...
@@ -287,6 +300,8 @@ export const TRANSLATIONS = {
cloud_replay_error
:
'
播放录像出错
'
,
cloud_replay_playing
:
'
正在观看云录像
'
,
cloud_replay_hint
:
'
以下是您近期的云录像,请在菜单中选择一条继续。
'
,
cloud_replay_delay_part1
:
'
本场比赛云录像:
'
,
cloud_replay_delay_part2
:
'
。将于本局结束后可播放。
'
,
cloud_replay_detail_time
:
'
时间:
'
,
cloud_replay_detail_players
:
'
对局:
'
,
cloud_replay_detail_score
:
'
比分:
'
,
...
...
@@ -296,9 +311,18 @@ export const TRANSLATIONS = {
cloud_replay_menu_play
:
'
播放云录像
'
,
cloud_replay_menu_download_yrp
:
'
下载 YRP 录像
'
,
cloud_replay_menu_back
:
'
返回
'
,
challonge_user_not_found
:
'
未找到你的参赛信息。
'
,
challonge_match_load_failed
:
'
读取比赛信息失败。
'
,
challonge_match_not_found
:
'
你没有当前轮次的比赛。
'
,
challonge_match_already_finished
:
'
你在当前轮次的比赛已经结束,如需重赛,请联系裁判。
'
,
challonge_match_created
:
'
已建立比赛专用房间,将会自动匹配你的对手。
'
,
challonge_player_already_in
:
'
请不要重复加入比赛房间。
'
,
death_cancel
:
'
已取消本房间的加时赛状态。
'
,
death_start
:
'
加时赛开始,从本回合开始计算4回合,基本分高的玩家获得本次决斗的胜利。
'
,
death_start_siding
:
'
加时赛开始,下次决斗的第4回合结束时,基本分高的玩家决斗胜利。
'
,
death_start
:
'
加时赛开始,从本回合开始计算4回合,基本分高的玩家获得本次决斗的胜利。
'
,
death_start_siding
:
'
加时赛开始,下次决斗的第4回合结束时,基本分高的玩家决斗胜利。
'
,
death_start_final
:
'
本次决斗将进入猝死赛,基本分发生变动的回合结束时,基本分高的玩家将获得本次决斗的胜利。
'
,
death_start_extra
:
...
...
src/feats/challonge-api.ts
0 → 100644
View file @
3ca2aca1
import
axios
,
{
AxiosInstance
}
from
'
axios
'
;
import
PQueue
from
'
p-queue
'
;
export
interface
Match
{
id
:
number
;
state
:
'
pending
'
|
'
open
'
|
'
complete
'
;
player1_id
:
number
;
player2_id
:
number
;
winner_id
?:
number
|
'
tie
'
;
scores_csv
?:
string
;
}
export
interface
MatchWrapper
{
match
:
Match
;
}
export
interface
Participant
{
id
:
number
;
name
:
string
;
deckbuf
?:
string
;
}
export
interface
ParticipantWrapper
{
participant
:
Participant
;
}
export
interface
Tournament
{
id
:
number
;
participants
:
ParticipantWrapper
[];
matches
:
MatchWrapper
[];
}
export
interface
TournamentWrapper
{
tournament
:
Tournament
;
}
export
interface
MatchPost
{
scores_csv
:
string
;
winner_id
?:
number
|
'
tie
'
;
}
export
interface
ChallongeConfig
{
api_key
:
string
;
tournament_id
:
string
;
cache_ttl
:
number
;
challonge_url
:
string
;
}
export
type
ChallongeLogger
=
{
info
:
(...
args
:
unknown
[])
=>
void
;
warn
:
(...
args
:
unknown
[])
=>
void
;
error
:
(...
args
:
unknown
[])
=>
void
;
};
const
NOOP_LOGGER
:
ChallongeLogger
=
{
info
:
()
=>
{},
warn
:
()
=>
{},
error
:
()
=>
{},
};
export
class
Challonge
{
constructor
(
private
config
:
ChallongeConfig
,
private
http
:
AxiosInstance
=
axios
.
create
(),
private
logger
:
ChallongeLogger
=
NOOP_LOGGER
,
)
{}
private
queue
=
new
PQueue
({
concurrency
:
1
});
private
previous
?:
Tournament
;
private
previousTime
=
0
;
private
get
tournamentEndpoint
()
{
const
root
=
this
.
config
.
challonge_url
.
replace
(
/
\/
+$/
,
''
);
return
`
${
root
}
/v1/tournaments/
${
this
.
config
.
tournament_id
}
.json`
;
}
private
async
getTournamentProcess
(
noCache
=
false
)
{
const
now
=
Date
.
now
();
if
(
!
noCache
&&
this
.
previous
&&
now
-
this
.
previousTime
<=
this
.
config
.
cache_ttl
)
{
return
this
.
previous
;
}
try
{
const
{
data
:
{
tournament
},
}
=
await
this
.
http
.
get
<
TournamentWrapper
>
(
this
.
tournamentEndpoint
,
{
params
:
{
api_key
:
this
.
config
.
api_key
,
include_participants
:
1
,
include_matches
:
1
,
},
timeout
:
5000
,
});
this
.
previous
=
tournament
;
this
.
previousTime
=
Date
.
now
();
return
tournament
;
}
catch
(
e
:
unknown
)
{
this
.
logger
.
error
(
`Failed to get tournament
${
this
.
config
.
tournament_id
}
:
${
String
(
e
)}
`
,
);
return
undefined
;
}
}
async
getTournament
(
noCache
=
false
)
{
if
(
noCache
)
{
return
this
.
getTournamentProcess
(
noCache
);
}
return
this
.
queue
.
add
(()
=>
this
.
getTournamentProcess
(
noCache
));
}
async
putScore
(
matchId
:
number
,
match
:
MatchPost
,
retried
=
0
)
{
try
{
const
root
=
this
.
config
.
challonge_url
.
replace
(
/
\/
+$/
,
''
);
await
this
.
http
.
put
(
`
${
root
}
/v1/tournaments/
${
this
.
config
.
tournament_id
}
/matches/
${
matchId
}
.json`
,
{
api_key
:
this
.
config
.
api_key
,
match
,
},
);
this
.
previous
=
undefined
;
this
.
previousTime
=
0
;
return
true
;
}
catch
(
e
:
unknown
)
{
this
.
logger
.
error
(
`Failed to put score for match
${
matchId
}
:
${
String
(
e
)}
`
,
);
if
(
retried
<
5
)
{
this
.
logger
.
info
(
`Retrying match
${
matchId
}
`
);
return
this
.
putScore
(
matchId
,
match
,
retried
+
1
);
}
this
.
logger
.
error
(
`Failed to put score for match
${
matchId
}
after 5 retries`
,
);
return
false
;
}
}
async
clearParticipants
()
{
try
{
const
root
=
this
.
config
.
challonge_url
.
replace
(
/
\/
+$/
,
''
);
await
this
.
http
.
delete
(
`
${
root
}
/v1/tournaments/
${
this
.
config
.
tournament_id
}
/participants/clear.json`
,
{
params
:
{
api_key
:
this
.
config
.
api_key
,
},
validateStatus
:
()
=>
true
,
},
);
this
.
previous
=
undefined
;
this
.
previousTime
=
0
;
return
true
;
}
catch
(
e
:
unknown
)
{
this
.
logger
.
error
(
`Failed to clear participants for tournament
${
this
.
config
.
tournament_id
}
:
${
String
(
e
)}
`
,
);
return
false
;
}
}
async
uploadParticipants
(
participants
:
{
name
:
string
;
deckbuf
?:
string
}[])
{
try
{
const
root
=
this
.
config
.
challonge_url
.
replace
(
/
\/
+$/
,
''
);
await
this
.
http
.
post
(
`
${
root
}
/v1/tournaments/
${
this
.
config
.
tournament_id
}
/participants/bulk_add.json`
,
{
api_key
:
this
.
config
.
api_key
,
participants
,
},
);
this
.
previous
=
undefined
;
this
.
previousTime
=
0
;
return
true
;
}
catch
(
e
:
unknown
)
{
this
.
logger
.
error
(
`Failed to upload participants for tournament
${
this
.
config
.
tournament_id
}
:
${
String
(
e
)}
`
,
);
return
false
;
}
}
}
src/feats/challonge-service.ts
0 → 100644
View file @
3ca2aca1
This diff is collapsed.
Click to expand it.
src/feats/cloud-replay/cloud-replay-service.ts
View file @
3ca2aca1
...
...
@@ -153,6 +153,7 @@ export class CloudReplayService {
});
await
duelRecordRepo
.
save
(
record
);
await
this
.
trySendTournamentReplayHint
(
room
,
record
.
id
);
}
catch
(
error
)
{
this
.
logger
.
warn
(
{
...
...
@@ -164,6 +165,16 @@ export class CloudReplayService {
}
}
private
async
trySendTournamentReplayHint
(
room
:
Room
,
replayId
:
number
)
{
if
(
!
this
.
ctx
.
config
.
getBoolean
(
'
TOURNAMENT_MODE
'
))
{
return
;
}
await
room
.
sendChat
(
`#{cloud_replay_delay_part1}R#
${
replayId
}
#{cloud_replay_delay_part2}`
,
ChatColor
.
BABYBLUE
,
);
}
private
buildPlayerRecord
(
room
:
Room
,
client
:
Client
,
...
...
src/feats/feats-module.ts
View file @
3ca2aca1
...
...
@@ -19,6 +19,7 @@ import { LockDeckService } from './lock-deck';
import
{
BlockReplay
}
from
'
./block-replay
'
;
import
{
RoomDeathService
}
from
'
./room-death-service
'
;
import
{
RoomAutoDeathService
}
from
'
./room-auto-death-service
'
;
import
{
ChallongeService
}
from
'
./challonge-service
'
;
export
const
FeatsModule
=
createAppContext
()
.
provide
(
ClientKeyProvider
)
...
...
@@ -35,6 +36,7 @@ export const FeatsModule = createAppContext()
.
provide
(
LpLowHintService
)
// low LP hint in duel
.
provide
(
RoomDeathService
)
// srvpro-style death mode (model 2)
.
provide
(
RoomAutoDeathService
)
// auto trigger death mode after duel start
.
provide
(
ChallongeService
)
// challonge deck lock + score sync
.
provide
(
LockDeckService
)
// srvpro-style tournament deck lock check
.
provide
(
RefreshFieldService
)
.
provide
(
Reconnect
)
...
...
src/feats/index.ts
View file @
3ca2aca1
...
...
@@ -2,6 +2,8 @@ export * from './client-version-check';
export
*
from
'
./client-key-provider
'
;
export
*
from
'
./chatgpt-service
'
;
export
*
from
'
./block-replay
'
;
export
*
from
'
./challonge-api
'
;
export
*
from
'
./challonge-service
'
;
export
*
from
'
./cloud-replay
'
;
export
*
from
'
./hide-player-name-provider
'
;
export
*
from
'
./lock-deck
'
;
...
...
src/feats/random-duel/provider.ts
View file @
3ca2aca1
...
...
@@ -243,12 +243,6 @@ export class RandomDuelProvider {
);
if
(
found
)
{
const
foundType
=
found
.
randomType
||
type
||
this
.
defaultType
;
found
.
randomType
=
foundType
;
found
.
hidePlayerNames
=
this
.
hidePlayerNameProvider
.
enabled
;
found
.
randomDuelDeprecated
=
joinState
.
deprecated
;
found
.
checkChatBadword
=
true
;
found
.
noHost
=
true
;
found
.
randomDuelMaxPlayer
=
this
.
resolveRandomDuelMaxPlayer
(
foundType
);
found
.
welcome
=
'
#{random_duel_enter_room_waiting}
'
;
this
.
applyWelcomeType
(
found
,
foundType
);
return
{
room
:
found
};
...
...
src/feats/windbot/join-windbot-ai.ts
View file @
3ca2aca1
...
...
@@ -39,7 +39,6 @@ export class JoinWindbotAi {
const
existingRoom
=
this
.
roomManager
.
findByName
(
normalizedPass
);
if
(
existingRoom
)
{
existingRoom
.
noHost
=
true
;
await
existingRoom
.
join
(
client
);
return
true
;
}
...
...
src/join-handlers/challonge-join-handler.ts
0 → 100644
View file @
3ca2aca1
import
{
ChatColor
,
YGOProCtosJoinGame
}
from
'
ygopro-msg-encode
'
;
import
{
Context
}
from
'
../app
'
;
import
{
ChallongeService
}
from
'
../feats
'
;
import
{
DuelStage
,
Room
,
RoomManager
}
from
'
../room
'
;
export
class
ChallongeJoinHandler
{
private
logger
=
this
.
ctx
.
createLogger
(
this
.
constructor
.
name
);
private
challongeService
=
this
.
ctx
.
get
(()
=>
ChallongeService
);
private
roomManager
=
this
.
ctx
.
get
(()
=>
RoomManager
);
constructor
(
private
ctx
:
Context
)
{
this
.
ctx
.
middleware
(
YGOProCtosJoinGame
,
async
(
msg
,
client
,
next
)
=>
{
if
(
!
this
.
challongeService
.
enabled
)
{
return
next
();
}
const
preRoom
=
this
.
resolvePreRoom
(
msg
.
pass
);
if
(
preRoom
&&
preRoom
.
duelStage
!==
DuelStage
.
Begin
)
{
return
preRoom
.
join
(
client
,
true
);
}
const
resolved
=
await
this
.
challongeService
.
resolveJoinInfo
(
client
.
name
);
if
(
resolved
.
ok
===
false
)
{
return
client
.
die
(
this
.
resolveJoinErrorMessage
(
resolved
.
reason
),
ChatColor
.
RED
,
);
}
const
roomName
=
this
.
resolveRoomName
(
resolved
.
match
.
id
);
const
room
=
await
this
.
roomManager
.
findOrCreateByName
(
roomName
);
room
.
noHost
=
true
;
room
.
challongeInfo
=
resolved
.
match
;
room
.
welcome
=
'
#{challonge_match_created}
'
;
if
(
this
.
hasSameParticipantInRoom
(
room
,
resolved
.
participant
.
id
))
{
this
.
logger
.
debug
(
{
roomName
:
room
.
name
,
participantId
:
resolved
.
participant
.
id
,
clientName
:
client
.
name
,
},
'
Rejected duplicated challonge participant in room
'
,
);
return
client
.
die
(
'
#{challonge_player_already_in}
'
,
ChatColor
.
RED
);
}
client
.
challongeInfo
=
resolved
.
participant
;
return
room
.
join
(
client
);
});
}
private
resolveRoomName
(
matchId
:
number
)
{
if
(
this
.
ctx
.
config
.
getBoolean
(
'
CHALLONGE_NO_MATCH_MODE
'
))
{
return
`
${
matchId
}
`
;
}
return
`M#
${
matchId
}
`
;
}
private
resolvePreRoom
(
pass
:
string
|
undefined
)
{
const
roomName
=
(
pass
||
''
).
trim
();
if
(
!
roomName
)
{
return
undefined
;
}
return
this
.
roomManager
.
findByName
(
roomName
);
}
private
hasSameParticipantInRoom
(
room
:
Room
,
participantId
:
number
)
{
return
room
.
playingPlayers
.
some
(
(
player
)
=>
player
&&
player
.
challongeInfo
?.
id
===
participantId
,
);
}
private
resolveJoinErrorMessage
(
reason
:
'
match_load_failed
'
|
'
user_not_found
'
|
'
match_not_found
'
,
)
{
if
(
reason
===
'
match_load_failed
'
)
{
return
'
#{challonge_match_load_failed}
'
;
}
if
(
reason
===
'
user_not_found
'
)
{
return
'
#{challonge_user_not_found}
'
;
}
return
'
#{challonge_match_not_found}
'
;
}
}
src/join-handlers/join-handler-module.ts
View file @
3ca2aca1
...
...
@@ -14,20 +14,22 @@ import { JoinBlankPassWindbotAi } from './join-blank-pass-windbot-ai';
import
{
JoinBlankPassMenu
}
from
'
./join-blank-pass-menu
'
;
import
{
JoinRoomlist
}
from
'
./join-roomlist
'
;
import
{
JoinBotlist
}
from
'
./join-botlist
'
;
import
{
ChallongeJoinHandler
}
from
'
./challonge-join-handler
'
;
export
const
JoinHandlerModule
=
createAppContext
<
ContextState
>
()
.
provide
(
ClientVersionCheck
)
.
provide
(
JoinPrechecks
)
.
provide
(
JoinWindbotToken
)
.
provide
(
BadwordPlayerInfoChecker
)
.
provide
(
RandomDuelJoinHandler
)
.
provide
(
JoinWindbotAi
)
.
provide
(
JoinRoomIp
)
.
provide
(
CloudReplayJoinHandler
)
.
provide
(
JoinRoomlist
)
.
provide
(
JoinBotlist
)
.
provide
(
JoinRoom
)
.
provide
(
JoinBlankPassMenu
)
.
provide
(
JoinRoomIp
)
// IP
.
provide
(
CloudReplayJoinHandler
)
// R, R#, W, W#, YRP#
.
provide
(
ChallongeJoinHandler
)
// any
.
provide
(
RandomDuelJoinHandler
)
// M, T
.
provide
(
JoinWindbotAi
)
// AI, AI#
.
provide
(
JoinRoomlist
)
// L
.
provide
(
JoinBotlist
)
// B
.
provide
(
JoinRoom
)
// room pass
.
provide
(
JoinBlankPassMenu
)
// blank pass below
.
provide
(
JoinBlankPassRandomDuel
)
.
provide
(
JoinBlankPassWindbotAi
)
.
provide
(
JoinFallback
)
...
...
src/legacy-api/legacy-api-deck-service.ts
View file @
3ca2aca1
...
...
@@ -3,6 +3,7 @@ import { LegacyDeckEntity } from './legacy-deck.entity';
import
{
decodeDeckBase64
,
encodeDeckBase64
}
from
'
../feats/cloud-replay
'
;
import
YGOProDeck
from
'
ygopro-deck-encode
'
;
import
{
LockDeckExpectedDeckCheck
}
from
'
../feats/lock-deck
'
;
import
{
ChallongeParticipantUpload
,
ChallongeService
}
from
'
../feats
'
;
import
{
deckNameMatch
}
from
'
./utility/deck-name-match
'
;
import
{
getDeckNameExactCandidates
,
...
...
@@ -32,6 +33,7 @@ type DeckDashboardStreamConnection = {
export
class
LegacyApiDeckService
{
private
logger
=
this
.
ctx
.
createLogger
(
'
LegacyApiDeckService
'
);
private
challongeService
=
this
.
ctx
.
get
(()
=>
ChallongeService
);
private
streamConnections
=
new
Map
<
string
,
DeckDashboardStreamConnection
>
();
private
backgrounds
:
DeckDashboardBg
[]
=
[{
url
:
''
,
desc
:
''
}];
private
bgRefreshedAt
=
0
;
...
...
@@ -199,7 +201,22 @@ export class LegacyApiDeckService {
koaCtx
.
body
=
'
Auth Failed.
'
;
return
;
}
this
.
sendDeckDashboardMessage
(
'
未开启Challonge模式。
'
);
this
.
sendDeckDashboardMessage
(
'
开始读取玩家列表。
'
);
const
participants
=
await
this
.
loadChallongeParticipantsFromDecks
();
if
(
!
participants
.
length
)
{
this
.
sendDeckDashboardMessage
(
'
玩家列表为空。
'
);
koaCtx
.
body
=
'
操作完成。
'
;
return
;
}
this
.
sendDeckDashboardMessage
(
`读取玩家列表完毕,共有
${
participants
.
length
}
名玩家。`
,
);
for
await
(
const
text
of
this
.
challongeService
.
uploadToChallonge
(
participants
,
))
{
this
.
sendDeckDashboardMessage
(
text
);
}
koaCtx
.
body
=
'
操作完成。
'
;
});
...
...
@@ -420,6 +437,52 @@ export class LegacyApiDeckService {
await
repo
.
save
(
row
);
}
private
async
loadChallongeParticipantsFromDecks
()
{
const
repo
=
this
.
getDeckRepo
();
if
(
!
repo
)
{
return
[]
as
ChallongeParticipantUpload
[];
}
const
rows
=
await
repo
.
find
({
order
:
{
uploadTime
:
'
DESC
'
,
id
:
'
DESC
'
,
},
});
const
loaded
=
new
Set
<
string
>
();
const
participants
:
ChallongeParticipantUpload
[]
=
[];
for
(
const
row
of
rows
)
{
const
name
=
this
.
toChallongeParticipantName
(
row
.
name
);
if
(
!
name
||
loaded
.
has
(
name
))
{
continue
;
}
try
{
const
deck
=
decodeDeckBase64
(
row
.
payload
,
row
.
mainc
);
participants
.
push
({
name
,
deckbuf
:
Buffer
.
from
(
deck
.
toUpdateDeckPayload
()).
toString
(
'
base64
'
),
});
loaded
.
add
(
name
);
}
catch
(
error
:
unknown
)
{
this
.
logger
.
warn
(
{
deckName
:
row
.
name
,
err
:
error
,
},
'
Failed to decode legacy deck for challonge upload
'
,
);
}
}
return
participants
;
}
private
toChallongeParticipantName
(
deckName
:
string
)
{
if
(
deckName
.
endsWith
(
'
.ydk
'
))
{
return
deckName
.
slice
(
0
,
-
4
);
}
return
deckName
;
}
private
addStreamConnection
(
ip
:
string
,
response
:
ServerResponse
)
{
this
.
closeStreamConnection
(
ip
,
'
replaced_by_same_ip
'
);
...
...
@@ -432,7 +495,10 @@ export class LegacyApiDeckService {
});
}
private
removeStreamConnection
(
ip
:
string
,
expectedResponse
?:
ServerResponse
)
{
private
removeStreamConnection
(
ip
:
string
,
expectedResponse
?:
ServerResponse
,
)
{
const
connection
=
this
.
streamConnections
.
get
(
ip
);
if
(
!
connection
)
{
return
;
...
...
@@ -559,7 +625,10 @@ export class LegacyApiDeckService {
const
body
=
response
.
data
as
any
;
const
images
=
Array
.
isArray
(
body
?.
images
)
?
body
.
images
:
[];
if
(
!
images
.
length
)
{
this
.
logger
.
warn
({
body
},
'
Deck dashboard background API returned no images
'
);
this
.
logger
.
warn
(
{
body
},
'
Deck dashboard background API returned no images
'
,
);
return
;
}
const
next
=
images
...
...
@@ -575,7 +644,9 @@ export class LegacyApiDeckService {
})
.
filter
((
item
):
item
is
DeckDashboardBg
=>
!!
item
);
if
(
!
next
.
length
)
{
this
.
logger
.
warn
(
'
Deck dashboard background API parse produced no valid result
'
);
this
.
logger
.
warn
(
'
Deck dashboard background API parse produced no valid result
'
,
);
return
;
}
this
.
backgrounds
=
next
;
...
...
src/legacy-api/legacy-api-service.ts
View file @
3ca2aca1
...
...
@@ -86,7 +86,9 @@ export class LegacyApiService {
this
.
addApiMessageHandler
(
'
death
'
,
'
start_death
'
,
async
(
value
)
=>
{
const
roomDeathService
=
this
.
ctx
.
get
(()
=>
RoomDeathService
);
const
foundRooms
=
value
===
'
all
'
?
this
.
ctx
.
get
(()
=>
RoomManager
).
allRooms
()
:
this
.
findRoomByTarget
(
value
);
value
===
'
all
'
?
this
.
ctx
.
get
(()
=>
RoomManager
).
allRooms
()
:
this
.
findRoomByTarget
(
value
);
if
(
!
foundRooms
.
length
)
{
return
[
'
room not found
'
,
value
];
}
...
...
@@ -105,7 +107,9 @@ export class LegacyApiService {
this
.
addApiMessageHandler
(
'
deathcancel
'
,
'
start_death
'
,
async
(
value
)
=>
{
const
roomDeathService
=
this
.
ctx
.
get
(()
=>
RoomDeathService
);
const
foundRooms
=
value
===
'
all
'
?
this
.
ctx
.
get
(()
=>
RoomManager
).
allRooms
()
:
this
.
findRoomByTarget
(
value
);
value
===
'
all
'
?
this
.
ctx
.
get
(()
=>
RoomManager
).
allRooms
()
:
this
.
findRoomByTarget
(
value
);
if
(
!
foundRooms
.
length
)
{
return
[
'
room not found
'
,
value
];
}
...
...
src/legacy-api/legacy-ban-service.ts
View file @
3ca2aca1
...
...
@@ -24,7 +24,9 @@ export class LegacyBanService {
return
client
.
die
(
'
#{banned_user_login}
'
,
ChatColor
.
RED
);
}
const
ipBan
=
client
.
ip
?
await
this
.
findBanRecord
({
ip
:
client
.
ip
})
:
null
;
const
ipBan
=
client
.
ip
?
await
this
.
findBanRecord
({
ip
:
client
.
ip
})
:
null
;
if
(
ipBan
)
{
this
.
logger
.
info
(
{
name
:
client
.
name
,
ip
:
client
.
ip
},
...
...
src/legacy-api/utility/deck-name-match.ts
View file @
3ca2aca1
export
function
deckNameMatch
(
deckName
:
string
,
playerName
:
string
)
{
if
(
deckName
===
playerName
||
deckName
===
`
${
playerName
}
.ydk`
||
deckName
===
`
${
playerName
}
.ydk.ydk`
)
{
return
true
;
}
const
parsedDeckName
=
deckName
.
match
(
/^
([^\+
\u
ff0b
]
+
)[\+
\u
ff0b
](
.+
?)(\.
ydk
){0,2}
$/
,
);
return
!!
(
parsedDeckName
&&
(
playerName
===
parsedDeckName
[
1
]
||
playerName
===
parsedDeckName
[
2
])
);
}
export
{
deckNameMatch
}
from
'
../../utility/deck-name-match
'
;
src/room/room.ts
View file @
3ca2aca1
...
...
@@ -371,12 +371,16 @@ export class Room {
}
}
async
join
(
client
:
Client
)
{
async
join
(
client
:
Client
,
toObserver
=
false
)
{
client
.
roomName
=
this
.
name
;
client
.
isHost
=
this
.
noHost
?
false
:
!
this
.
allPlayers
.
length
;
const
firstEmptyPlayerSlot
=
this
.
players
.
findIndex
((
p
)
=>
!
p
);
const
isPlayer
=
firstEmptyPlayerSlot
>=
0
&&
this
.
duelStage
===
DuelStage
.
Begin
;
!
toObserver
&&
firstEmptyPlayerSlot
>=
0
&&
this
.
duelStage
===
DuelStage
.
Begin
;
client
.
isHost
=
this
.
noHost
?
false
:
isPlayer
&&
!
this
.
playingPlayers
.
length
;
if
(
isPlayer
)
{
this
.
players
[
firstEmptyPlayerSlot
]
=
client
;
...
...
src/services/typeorm.ts
View file @
3ca2aca1
...
...
@@ -35,7 +35,6 @@ export const TypeormFactory = async (ctx: AppContext) => {
const
password
=
config
.
getString
(
'
DB_PASS
'
);
const
database
=
config
.
getString
(
'
DB_NAME
'
);
const
synchronize
=
!
config
.
getBoolean
(
'
DB_NO_INIT
'
);
const
dataSource
=
new
DataSource
({
type
:
'
postgres
'
,
...
...
src/utility/deck-name-match.ts
0 → 100644
View file @
3ca2aca1
export
function
deckNameMatch
(
deckName
:
string
,
playerName
:
string
)
{
if
(
deckName
===
playerName
||
deckName
===
`
${
playerName
}
.ydk`
||
deckName
===
`
${
playerName
}
.ydk.ydk`
)
{
return
true
;
}
const
parsedDeckName
=
deckName
.
match
(
/^
([^\+
\u
ff0b
]
+
)[\+
\u
ff0b
](
.+
?)(\.
ydk
){0,2}
$/
,
);
return
!!
(
parsedDeckName
&&
(
playerName
===
parsedDeckName
[
1
]
||
playerName
===
parsedDeckName
[
2
])
);
}
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