Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
S
srvpro
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
srvpro
Commits
e6edd444
Commit
e6edd444
authored
Sep 12, 2021
by
nanahira
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into ai-play
parents
92c9fa5f
55f93dc3
Pipeline
#5514
passed with stages
in 41 minutes and 42 seconds
Changes
16
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
900 additions
and
475 deletions
+900
-475
.gitignore
.gitignore
+1
-0
.gitlab-ci.yml
.gitlab-ci.yml
+117
-31
Dockerfile
Dockerfile
+7
-34
Dockerfile.lite
Dockerfile.lite
+22
-7
Replay.coffee
Replay.coffee
+3
-2
Replay.js
Replay.js
+4
-3
YGOProMessages.ts
YGOProMessages.ts
+23
-5
data-manager/DataManager.js
data-manager/DataManager.js
+14
-5
data-manager/DataManager.ts
data-manager/DataManager.ts
+14
-5
data/default_config.json
data/default_config.json
+25
-11
roomlist.coffee
roomlist.coffee
+2
-2
roomlist.js
roomlist.js
+2
-2
ygopro-server.coffee
ygopro-server.coffee
+292
-165
ygopro-server.js
ygopro-server.js
+328
-194
ygopro.coffee
ygopro.coffee
+15
-4
ygopro.js
ygopro.js
+31
-5
No files found.
.gitignore
View file @
e6edd444
...
@@ -32,6 +32,7 @@ test*
...
@@ -32,6 +32,7 @@ test*
*.tmp
*.tmp
*.bak
*.bak
*.log
*.log
log.*
*.map
*.map
...
...
.gitlab-ci.yml
View file @
e6edd444
stages
:
stages
:
-
build
-
build
-
build2
-
combine
-
deploy
-
deploy
variables
:
variables
:
GIT_DEPTH
:
"
1"
GIT_DEPTH
:
"
1"
CONTAINER_TEST_IMAGE
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
CONTAINER_TEST_IMAGE
_FULL
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-full
CONTAINER_TEST_IMAGE_LITE
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-lite
CONTAINER_TEST_IMAGE_LITE
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-lite
CONTAINER_TEST_IMAGE_X86_FULL
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86-full
CONTAINER_TEST_IMAGE_X86_LITE
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86-lite
CONTAINER_TEST_IMAGE_ARM_FULL
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm-full
CONTAINER_TEST_IMAGE_ARM_LITE
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm-lite
CONTAINER_RELEASE_IMAGE
:
$CI_REGISTRY_IMAGE:latest
CONTAINER_RELEASE_IMAGE
:
$CI_REGISTRY_IMAGE:latest
CONTAINER_RELEASE_IMAGE_FULL
:
$CI_REGISTRY_IMAGE:full
CONTAINER_RELEASE_IMAGE_LITE
:
$CI_REGISTRY_IMAGE:lite
CONTAINER_RELEASE_IMAGE_LITE
:
$CI_REGISTRY_IMAGE:lite
build
:
build
_lite_x86
:
stage
:
build
stage
:
build
tags
:
tags
:
-
docker
-
docker
script
:
script
:
-
TARGET_IMAGE=$CONTAINER_TEST_IMAGE_X86_LITE
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker build --pull --no-cache -t $CONTAINER_TEST_IMAGE .
-
docker build --pull --no-cache -f ./Dockerfile.lite -t $TARGET_IMAGE .
-
docker push $CONTAINER_TEST_IMAGE
-
docker push $TARGET_IMAGE
build_lite_arm
:
build_lite
:
stage
:
build
stage
:
build
tags
:
tags
:
-
docker-arm
script
:
-
TARGET_IMAGE=$CONTAINER_TEST_IMAGE_ARM_LITE
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker build --pull --no-cache -f ./Dockerfile.lite -t $TARGET_IMAGE .
-
docker push $TARGET_IMAGE
build_full_x86
:
stage
:
build2
tags
:
-
docker
script
:
-
TARGET_IMAGE=$CONTAINER_TEST_IMAGE_X86_FULL
-
SOURCE_IMAGE=$CONTAINER_TEST_IMAGE_X86_LITE
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker build --build-arg BASE_IMAGE=$SOURCE_IMAGE --pull --no-cache -t
$TARGET_IMAGE .
-
docker push $TARGET_IMAGE
build_full_arm
:
stage
:
build2
tags
:
-
docker-arm
script
:
-
TARGET_IMAGE=$CONTAINER_TEST_IMAGE_ARM_FULL
-
SOURCE_IMAGE=$CONTAINER_TEST_IMAGE_ARM_LITE
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker build --build-arg BASE_IMAGE=$SOURCE_IMAGE --pull --no-cache -t
$TARGET_IMAGE .
-
docker push $TARGET_IMAGE
combine_lite
:
stage
:
build2
tags
:
-
docker
script
:
-
TARGET_IMAGE=$CONTAINER_TEST_IMAGE_LITE
-
SOURCE_IMAGE_2=$CONTAINER_TEST_IMAGE_ARM_LITE
-
SOURCE_IMAGE_1=$CONTAINER_TEST_IMAGE_X86_LITE
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker pull $SOURCE_IMAGE_1
-
docker pull $SOURCE_IMAGE_2
-
docker manifest create $TARGET_IMAGE --amend $SOURCE_IMAGE_1 --amend
$SOURCE_IMAGE_2
-
docker manifest push $TARGET_IMAGE
combine_full
:
stage
:
combine
tags
:
-
docker
-
docker
script
:
script
:
-
TARGET_IMAGE=$CONTAINER_TEST_IMAGE_FULL
-
SOURCE_IMAGE_2=$CONTAINER_TEST_IMAGE_ARM_FULL
-
SOURCE_IMAGE_1=$CONTAINER_TEST_IMAGE_X86_FULL
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker build --pull --no-cache -f ./Dockerfile.lite -t $CONTAINER_TEST_IMAGE_LITE .
-
docker pull $SOURCE_IMAGE_1
-
docker push $CONTAINER_TEST_IMAGE_LITE
-
docker pull $SOURCE_IMAGE_2
-
docker manifest create $TARGET_IMAGE --amend $SOURCE_IMAGE_1 --amend
$SOURCE_IMAGE_2
-
docker manifest push $TARGET_IMAGE
#upload_stuff_to_minio:
#upload_stuff_to_minio:
# stage: deploy
# stage: deploy
# tags:
# tags:
# - linux
# - linux
# image: $CONTAINER_TEST_IMAGE
# image: $CONTAINER_TEST_IMAGE
_FULL
# script:
# script:
# - apt update ; apt -y install python3-pip
# - apt update ; apt -y install python3-pip
# - pip3 install -U -i https://mirrors.aliyun.com/pypi/simple/ awscli
# - pip3 install -U -i https://mirrors.aliyun.com/pypi/simple/ awscli
...
@@ -42,35 +102,61 @@ build_lite:
...
@@ -42,35 +102,61 @@ build_lite:
# only:
# only:
# - master
# - master
deploy_latest
:
deploy_latest
_full
:
stage
:
deploy
stage
:
deploy
tags
:
tags
:
-
docker
-
docker
script
:
script
:
-
TARGET_IMAGE_2=$CONTAINER_RELEASE_IMAGE
-
TARGET_IMAGE=$CONTAINER_RELEASE_IMAGE_FULL
-
SOURCE_IMAGE=$CONTAINER_TEST_IMAGE_FULL
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker pull $CONTAINER_TEST_IMAGE
-
docker pull $SOURCE_IMAGE
-
docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
-
docker tag $SOURCE_IMAGE $TARGET_IMAGE
-
docker push $CONTAINER_RELEASE_IMAGE
-
docker push $TARGET_IMAGE
-
docker pull $CONTAINER_TEST_IMAGE_LITE
-
docker tag $SOURCE_IMAGE $TARGET_IMAGE_2
-
docker tag $CONTAINER_TEST_IMAGE_LITE $CONTAINER_RELEASE_IMAGE_LITE
-
docker push $TARGET_IMAGE_2
-
docker push $CONTAINER_RELEASE_IMAGE_LITE
only
:
only
:
-
master
-
master
deploy_latest_lite
:
deploy_tag
:
stage
:
deploy
tags
:
-
docker
script
:
-
TARGET_IMAGE=$CONTAINER_RELEASE_IMAGE_LITE
-
SOURCE_IMAGE=$CONTAINER_TEST_IMAGE_LITE
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker pull $SOURCE_IMAGE
-
docker tag $SOURCE_IMAGE $TARGET_IMAGE
-
docker push $TARGET_IMAGE
only
:
-
master
deploy_tag_full
:
stage
:
deploy
tags
:
-
docker
script
:
-
TARGET_IMAGE_2=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
-
TARGET_IMAGE=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-full
-
SOURCE_IMAGE=$CONTAINER_TEST_IMAGE_FULL
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker pull $SOURCE_IMAGE
-
docker tag $SOURCE_IMAGE $TARGET_IMAGE
-
docker push $TARGET_IMAGE
-
docker tag $SOURCE_IMAGE $TARGET_IMAGE_2
-
docker push $TARGET_IMAGE_2
only
:
-
tags
deploy_tag_lite
:
stage
:
deploy
stage
:
deploy
tags
:
tags
:
-
docker
-
docker
variables
:
CONTAINER_TAG_IMAGE
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
CONTAINER_TAG_IMAGE_LITE
:
$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-lite
script
:
script
:
-
TARGET_IMAGE=$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-lite
-
SOURCE_IMAGE=$CONTAINER_TEST_IMAGE_LITE
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
docker pull $CONTAINER_TEST_IMAGE
-
docker pull $SOURCE_IMAGE
-
docker tag $CONTAINER_TEST_IMAGE $CONTAINER_TAG_IMAGE
-
docker tag $SOURCE_IMAGE $TARGET_IMAGE
-
docker push $CONTAINER_TAG_IMAGE
-
docker push $TARGET_IMAGE
-
docker pull $CONTAINER_TEST_IMAGE_LITE
-
docker tag $CONTAINER_TEST_IMAGE_LITE $CONTAINER_RELEASE_IMAGE_LITE
-
docker push $CONTAINER_RELEASE_IMAGE_LITE
only
:
only
:
-
tags
-
tags
Dockerfile
View file @
e6edd444
# Dockerfile for SRVPro
ARG
BASE_IMAGE=git-registry.mycard.moe/mycard/srvpro:lite
FROM
node:14-buster-slim
FROM
$BASE_IMAGE
LABEL
Author="Nanahira <nanahira@momobako.com>"
RUN
npm
install
-g
pm2
# apt
RUN
apt update
&&
\
RUN
apt update
&&
\
env
DEBIAN_FRONTEND
=
noninteractive apt
install
-y
wget git build-essential libevent-dev libsqlite3-dev mono-complete p7zip-full python3 liblua5.3-dev
&&
\
apt
-y
install
mono-complete
&&
\
rm
-rf
/var/lib/apt/lists/
*
npm
install
-g
pm2
&&
\
rm
-rf
/tmp/
*
/var/tmp/
*
/var/lib/apt/lists/
*
# srvpro
COPY
. /ygopro-server
WORKDIR
/ygopro-server
RUN
npm ci
&&
\
mkdir
config decks replays logs /redis
# ygopro
RUN
git clone
--branch
=
server
--recursive
--depth
=
1 https://github.com/purerosefallen/ygopro
&&
\
cd
ygopro
&&
\
git submodule foreach git checkout master
&&
\
wget
-O
- https://github.com/premake/premake-core/releases/download/v5.0.0-alpha14/premake-5.0.0-alpha14-linux.tar.gz |
tar
zfx -
&&
\
./premake5 gmake
&&
\
cd
build
&&
\
make
config
=
release
-j
$(
nproc
)
&&
\
cd
..
&&
\
mv
./bin/release/ygopro
.
&&
\
strip ygopro
&&
\
mkdir
replay expansions
&&
\
rm
-rf
.git
*
bin obj build ocgcore cmake lua premake
*
sound textures .travis.yml
*
.txt appveyor.yml LICENSE README.md
*
.lua strings.conf system.conf
&&
\
ls
gframe |
sed
'/game.cpp/d'
| xargs
-I
{}
rm
-rf
gframe/
{}
# windbot
# windbot
RUN
git clone
--depth
=
1 https://
github.com/purerosefallen
/windbot /tmp/windbot
&&
\
RUN
git clone
--depth
=
1 https://
code.mycard.moe/nanahira
/windbot /tmp/windbot
&&
\
cd
/tmp/windbot
&&
\
cd
/tmp/windbot
&&
\
xbuild /property:Configuration
=
Release /property:TargetFrameworkVersion
=
"v4.5"
&&
\
xbuild /property:Configuration
=
Release /property:TargetFrameworkVersion
=
"v4.5"
&&
\
mv
/tmp/windbot/bin/Release /ygopro-server/windbot
&&
\
mv
/tmp/windbot/bin/Release /ygopro-server/windbot
&&
\
cp
-rf
/ygopro-server/ygopro/cards.cdb /ygopro-server/windbot/
&&
\
cp
-rf
/ygopro-server/ygopro/cards.cdb /ygopro-server/windbot/
&&
\
rm
-rf
/tmp/windbot
rm
-rf
/tmp/windbot
# infos
WORKDIR
/ygopro-server
EXPOSE
7911 7922 7933
# VOLUME [ /ygopro-server/config, /ygopro-server/decks, /ygopro-server/replays, /redis ]
CMD
[ "pm2-docker", "start", "/ygopro-server/data/pm2-docker.json" ]
CMD
[ "pm2-docker", "start", "/ygopro-server/data/pm2-docker.json" ]
Dockerfile.lite
View file @
e6edd444
# Dockerfile for SRVPro Lite
# Dockerfile for SRVPro Lite
FROM node:14-buster-slim
FROM debian:bullseye as premake-builder
RUN apt update && \
env DEBIAN_FRONTEND=noninteractive apt install -y wget build-essential p7zip-full && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /usr/src
RUN wget -O premake.zip https://github.com/premake/premake-core/releases/download/v5.0.0-alpha14/premake-5.0.0-alpha14-src.zip && \
7z x -y premake.zip && \
mv premake-5.0.0-alpha14 premake && \
cd premake/build/gmake.unix && \
make -j$(nproc)
FROM node:16-bullseye-slim
LABEL Author="Nanahira <nanahira@momobako.com>"
# apt
# apt
RUN apt update && \
RUN apt update && \
env DEBIAN_FRONTEND=noninteractive apt install -y wget git build-essential lib
sqlite3-dev libevent-dev p7zip-full
python3 liblua5.3-dev && \
env DEBIAN_FRONTEND=noninteractive apt install -y wget git build-essential lib
event-dev libsqlite3-dev p7zip-full python3 python-is-
python3 liblua5.3-dev && \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/*
/tmp/* /var/tmp/*
# srvpro
# srvpro
COPY . /ygopro-server
COPY . /ygopro-server
...
@@ -12,11 +26,12 @@ WORKDIR /ygopro-server
...
@@ -12,11 +26,12 @@ WORKDIR /ygopro-server
RUN npm ci && \
RUN npm ci && \
mkdir config decks replays logs
mkdir config decks replays logs
RUN git clone --branch=server --recursive --depth=1 https://github.com/purerosefallen/ygopro && \
COPY --from=premake-builder /usr/src/premake/bin/release/premake5 /usr/bin/premake5
RUN git clone --branch=server --recursive --depth=1 https://code.mycard.moe/nanahira/ygopro && \
cd ygopro && \
cd ygopro && \
git submodule foreach git checkout master && \
git submodule foreach git checkout master && \
wget -O - https://github.com/premake/premake-core/releases/download/v5.0.0-alpha13/premake-5.0.0-alpha13-linux.tar.gz | tar zfx - && \
premake5 gmake && \
./premake5 gmake && \
cd build && \
cd build && \
make config=release -j$(nproc) && \
make config=release -j$(nproc) && \
cd .. && \
cd .. && \
...
@@ -31,4 +46,4 @@ WORKDIR /ygopro-server
...
@@ -31,4 +46,4 @@ WORKDIR /ygopro-server
EXPOSE 7911 7922 7933
EXPOSE 7911 7922 7933
# VOLUME [ /ygopro-server/config, /ygopro-server/decks, /ygopro-server/replays ]
# VOLUME [ /ygopro-server/config, /ygopro-server/decks, /ygopro-server/replays ]
CMD [ "n
ode", "ygopro-server.js
" ]
CMD [ "n
pm", "start
" ]
Replay.coffee
View file @
e6edd444
...
@@ -122,9 +122,10 @@ class Replay
...
@@ -122,9 +122,10 @@ class Replay
@
fromBuffer
:
(
buffer
)
->
@
fromBuffer
:
(
buffer
)
->
reader
=
new
ReplayReader
buffer
reader
=
new
ReplayReader
buffer
header
=
Replay
.
readHeader
reader
header
=
Replay
.
readHeader
reader
lzmaBuffer
=
Buffer
.
concat
[
header
.
getLzmaHeader
(),
reader
.
readAll
()]
restBuffer
=
reader
.
readAll
()
lzmaBuffer
=
Buffer
.
concat
[
header
.
getLzmaHeader
(),
restBuffer
]
if
header
.
isCompressed
if
header
.
isCompressed
decompressed
=
lzma
Buffer
decompressed
=
rest
Buffer
else
else
decompressed
=
Buffer
.
from
lzma
.
decompress
lzmaBuffer
decompressed
=
Buffer
.
from
lzma
.
decompress
lzmaBuffer
reader
=
new
ReplayReader
decompressed
reader
=
new
ReplayReader
decompressed
...
...
Replay.js
View file @
e6edd444
...
@@ -180,12 +180,13 @@
...
@@ -180,12 +180,13 @@
}
}
static
fromBuffer
(
buffer
)
{
static
fromBuffer
(
buffer
)
{
var
decompressed
,
header
,
lzmaBuffer
,
reader
,
replay
;
var
decompressed
,
header
,
lzmaBuffer
,
reader
,
replay
,
restBuffer
;
reader
=
new
ReplayReader
(
buffer
);
reader
=
new
ReplayReader
(
buffer
);
header
=
Replay
.
readHeader
(
reader
);
header
=
Replay
.
readHeader
(
reader
);
lzmaBuffer
=
Buffer
.
concat
([
header
.
getLzmaHeader
(),
reader
.
readAll
()]);
restBuffer
=
reader
.
readAll
();
lzmaBuffer
=
Buffer
.
concat
([
header
.
getLzmaHeader
(),
restBuffer
]);
if
(
header
.
isCompressed
)
{
if
(
header
.
isCompressed
)
{
decompressed
=
lzma
Buffer
;
decompressed
=
rest
Buffer
;
}
else
{
}
else
{
decompressed
=
Buffer
.
from
(
lzma
.
decompress
(
lzmaBuffer
));
decompressed
=
Buffer
.
from
(
lzma
.
decompress
(
lzmaBuffer
));
}
}
...
...
YGOProMessages.ts
View file @
e6edd444
...
@@ -8,7 +8,7 @@ import net from "net";
...
@@ -8,7 +8,7 @@ import net from "net";
class
Handler
{
class
Handler
{
handler
:
(
buffer
:
Buffer
,
info
:
any
,
datas
:
Buffer
[],
params
:
any
)
=>
Promise
<
boolean
>
;
private
handler
:
(
buffer
:
Buffer
,
info
:
any
,
datas
:
Buffer
[],
params
:
any
)
=>
Promise
<
boolean
>
;
synchronous
:
boolean
;
synchronous
:
boolean
;
constructor
(
handler
:
(
buffer
:
Buffer
,
info
:
any
,
datas
:
Buffer
[],
params
:
any
)
=>
Promise
<
boolean
>
,
synchronous
:
boolean
)
{
constructor
(
handler
:
(
buffer
:
Buffer
,
info
:
any
,
datas
:
Buffer
[],
params
:
any
)
=>
Promise
<
boolean
>
,
synchronous
:
boolean
)
{
this
.
handler
=
handler
;
this
.
handler
=
handler
;
...
@@ -46,14 +46,32 @@ export interface HandleResult {
...
@@ -46,14 +46,32 @@ export interface HandleResult {
feedback
:
Feedback
;
feedback
:
Feedback
;
}
}
export
interface
Constants
{
TYPES
:
Record
<
string
,
number
>
;
RACES
:
Record
<
string
,
number
>
;
ATTRIBUTES
:
Record
<
string
,
number
>
;
LINK_MARKERS
:
Record
<
string
,
number
>
;
DUEL_STAGE
:
Record
<
string
,
number
>
;
COLORS
:
Record
<
string
,
number
>
;
TIMING
:
Record
<
string
,
string
>
;
NETWORK
:
Record
<
string
,
string
>
;
NETPLAYER
:
Record
<
string
,
string
>
;
CTOS
:
Record
<
string
,
string
>
;
STOC
:
Record
<
string
,
string
>
;
PLAYERCHANGE
:
Record
<
string
,
string
>
;
ERRMSG
:
Record
<
string
,
string
>
;
MODE
:
Record
<
string
,
string
>
;
MSG
:
Record
<
string
,
string
>
;
}
export
class
YGOProMessagesHelper
{
export
class
YGOProMessagesHelper
{
handlers
:
HandlerList
;
handlers
:
HandlerList
;
structs
:
Map
<
string
,
Struct
>
;
structs
:
Map
<
string
,
Struct
>
;
structs_declaration
:
any
;
structs_declaration
:
Record
<
string
,
Struct
>
;
typedefs
:
any
;
typedefs
:
Record
<
string
,
string
>
;
proto_structs
:
any
;
proto_structs
:
Record
<
'
CTOS
'
|
'
STOC
'
,
Record
<
string
,
string
>>
;
constants
:
any
;
constants
:
Constants
;
singleHandleLimit
:
number
;
singleHandleLimit
:
number
;
constructor
(
singleHandleLimit
?:
number
)
{
constructor
(
singleHandleLimit
?:
number
)
{
...
...
data-manager/DataManager.js
View file @
e6edd444
...
@@ -268,8 +268,8 @@ class DataManager {
...
@@ -268,8 +268,8 @@ class DataManager {
const
queryBuilder
=
repo
.
createQueryBuilder
(
"
duelLog
"
)
const
queryBuilder
=
repo
.
createQueryBuilder
(
"
duelLog
"
)
.
where
(
"
1
"
);
.
where
(
"
1
"
);
if
(
roomName
!=
null
&&
roomName
.
length
)
{
if
(
roomName
!=
null
&&
roomName
.
length
)
{
const
escapedRoomName
=
this
.
getEscapedString
(
roomName
);
//
const escapedRoomName = this.getEscapedString(roomName);
queryBuilder
.
andWhere
(
"
duelLog.name
like :escapedRoomName
"
,
{
escapedR
oomName
});
queryBuilder
.
andWhere
(
"
duelLog.name
= :roomName
"
,
{
r
oomName
});
}
}
if
(
duelCount
!=
null
&&
!
isNaN
(
duelCount
))
{
if
(
duelCount
!=
null
&&
!
isNaN
(
duelCount
))
{
queryBuilder
.
andWhere
(
"
duelLog.duelCount = :duelCount
"
,
{
duelCount
});
queryBuilder
.
andWhere
(
"
duelLog.duelCount = :duelCount
"
,
{
duelCount
});
...
@@ -278,9 +278,9 @@ class DataManager {
...
@@ -278,9 +278,9 @@ class DataManager {
let
innerQuery
=
"
select id from duel_log_player where duel_log_player.duelLogId = duelLog.id
"
;
let
innerQuery
=
"
select id from duel_log_player where duel_log_player.duelLogId = duelLog.id
"
;
const
innerQueryParams
=
{};
const
innerQueryParams
=
{};
if
(
playerName
!=
null
&&
playerName
.
length
)
{
if
(
playerName
!=
null
&&
playerName
.
length
)
{
const
escapedPlayerName
=
this
.
getEscapedString
(
playerName
);
//
const escapedPlayerName = this.getEscapedString(playerName);
innerQuery
+=
"
and duel_log_player.realName
like :escapedP
layerName
"
;
innerQuery
+=
"
and duel_log_player.realName
= :p
layerName
"
;
innerQueryParams
.
escapedPlayerName
=
escapedP
layerName
;
innerQueryParams
.
playerName
=
p
layerName
;
}
}
if
(
playerScore
!=
null
&&
!
isNaN
(
playerScore
))
{
if
(
playerScore
!=
null
&&
!
isNaN
(
playerScore
))
{
innerQuery
+=
"
and duel_log_player.score = :playerScore
"
;
innerQuery
+=
"
and duel_log_player.score = :playerScore
"
;
...
@@ -710,16 +710,25 @@ class DataManager {
...
@@ -710,16 +710,25 @@ class DataManager {
}
}
async
randomDuelPlayerWin
(
name
)
{
async
randomDuelPlayerWin
(
name
)
{
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
if
(
!
score
)
{
return
;
}
score
.
win
();
score
.
win
();
await
this
.
saveRandomDuelScore
(
score
);
await
this
.
saveRandomDuelScore
(
score
);
}
}
async
randomDuelPlayerLose
(
name
)
{
async
randomDuelPlayerLose
(
name
)
{
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
if
(
!
score
)
{
return
;
}
score
.
lose
();
score
.
lose
();
await
this
.
saveRandomDuelScore
(
score
);
await
this
.
saveRandomDuelScore
(
score
);
}
}
async
randomDuelPlayerFlee
(
name
)
{
async
randomDuelPlayerFlee
(
name
)
{
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
if
(
!
score
)
{
return
;
}
score
.
flee
();
score
.
flee
();
await
this
.
saveRandomDuelScore
(
score
);
await
this
.
saveRandomDuelScore
(
score
);
}
}
...
...
data-manager/DataManager.ts
View file @
e6edd444
...
@@ -276,8 +276,8 @@ export class DataManager {
...
@@ -276,8 +276,8 @@ export class DataManager {
const
queryBuilder
=
repo
.
createQueryBuilder
(
"
duelLog
"
)
const
queryBuilder
=
repo
.
createQueryBuilder
(
"
duelLog
"
)
.
where
(
"
1
"
);
.
where
(
"
1
"
);
if
(
roomName
!=
null
&&
roomName
.
length
)
{
if
(
roomName
!=
null
&&
roomName
.
length
)
{
const
escapedRoomName
=
this
.
getEscapedString
(
roomName
);
//
const escapedRoomName = this.getEscapedString(roomName);
queryBuilder
.
andWhere
(
"
duelLog.name
like :escapedRoomName
"
,
{
escapedR
oomName
});
queryBuilder
.
andWhere
(
"
duelLog.name
= :roomName
"
,
{
r
oomName
});
}
}
if
(
duelCount
!=
null
&&
!
isNaN
(
duelCount
))
{
if
(
duelCount
!=
null
&&
!
isNaN
(
duelCount
))
{
queryBuilder
.
andWhere
(
"
duelLog.duelCount = :duelCount
"
,
{
duelCount
});
queryBuilder
.
andWhere
(
"
duelLog.duelCount = :duelCount
"
,
{
duelCount
});
...
@@ -286,9 +286,9 @@ export class DataManager {
...
@@ -286,9 +286,9 @@ export class DataManager {
let
innerQuery
=
"
select id from duel_log_player where duel_log_player.duelLogId = duelLog.id
"
;
let
innerQuery
=
"
select id from duel_log_player where duel_log_player.duelLogId = duelLog.id
"
;
const
innerQueryParams
:
any
=
{};
const
innerQueryParams
:
any
=
{};
if
(
playerName
!=
null
&&
playerName
.
length
)
{
if
(
playerName
!=
null
&&
playerName
.
length
)
{
const
escapedPlayerName
=
this
.
getEscapedString
(
playerName
);
//
const escapedPlayerName = this.getEscapedString(playerName);
innerQuery
+=
"
and duel_log_player.realName
like :escapedP
layerName
"
;
innerQuery
+=
"
and duel_log_player.realName
= :p
layerName
"
;
innerQueryParams
.
escapedPlayerName
=
escapedP
layerName
;
innerQueryParams
.
playerName
=
p
layerName
;
}
}
if
(
playerScore
!=
null
&&
!
isNaN
(
playerScore
))
{
if
(
playerScore
!=
null
&&
!
isNaN
(
playerScore
))
{
innerQuery
+=
"
and duel_log_player.score = :playerScore
"
;
innerQuery
+=
"
and duel_log_player.score = :playerScore
"
;
...
@@ -714,16 +714,25 @@ export class DataManager {
...
@@ -714,16 +714,25 @@ export class DataManager {
}
}
async
randomDuelPlayerWin
(
name
:
string
)
{
async
randomDuelPlayerWin
(
name
:
string
)
{
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
if
(
!
score
)
{
return
;
}
score
.
win
();
score
.
win
();
await
this
.
saveRandomDuelScore
(
score
);
await
this
.
saveRandomDuelScore
(
score
);
}
}
async
randomDuelPlayerLose
(
name
:
string
)
{
async
randomDuelPlayerLose
(
name
:
string
)
{
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
if
(
!
score
)
{
return
;
}
score
.
lose
();
score
.
lose
();
await
this
.
saveRandomDuelScore
(
score
);
await
this
.
saveRandomDuelScore
(
score
);
}
}
async
randomDuelPlayerFlee
(
name
:
string
)
{
async
randomDuelPlayerFlee
(
name
:
string
)
{
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
const
score
=
await
this
.
getOrCreateRandomDuelScore
(
name
);
if
(
!
score
)
{
return
;
}
score
.
flee
();
score
.
flee
();
await
this
.
saveRandomDuelScore
(
score
);
await
this
.
saveRandomDuelScore
(
score
);
}
}
...
...
data/default_config.json
View file @
e6edd444
...
@@ -7,7 +7,7 @@
...
@@ -7,7 +7,7 @@
"lflist"
:
0
,
"lflist"
:
0
,
"rule"
:
0
,
"rule"
:
0
,
"mode"
:
0
,
"mode"
:
0
,
"comment"
:
"rule: 0=OCG
ONLY, 1=TCGONLY, 2=OT
; mode: 0=SINGLE, 1=MATCH, 2=TAG"
,
"comment"
:
"rule: 0=OCG
-ONLY, 1=TCG-ONLY, 2=SC-ONLY, 3=CUSTOM-ONLY, 4=NO-UNIQUE, 5=ALL
; mode: 0=SINGLE, 1=MATCH, 2=TAG"
,
"duel_rule"
:
5
,
"duel_rule"
:
5
,
"no_check_deck"
:
false
,
"no_check_deck"
:
false
,
"no_shuffle_deck"
:
false
,
"no_shuffle_deck"
:
false
,
...
@@ -20,8 +20,11 @@
...
@@ -20,8 +20,11 @@
},
},
"modules"
:
{
"modules"
:
{
"welcome"
:
"MyCard YGOPro Server"
,
"welcome"
:
"MyCard YGOPro Server"
,
"update"
:
"请更新游戏版本"
,
"update"
:
"请更新你的客户端版本"
,
"wait_update"
:
"你的客户端版本高于服务器版本,请等待服务器更新"
,
"stop"
:
false
,
"stop"
:
false
,
"full"
:
"服务器已爆满"
,
"max_rooms_count"
:
0
,
"side_timeout"
:
false
,
"side_timeout"
:
false
,
"tag_duel_surrender"
:
true
,
"tag_duel_surrender"
:
true
,
"replay_delay"
:
true
,
"replay_delay"
:
true
,
...
@@ -89,7 +92,17 @@
...
@@ -89,7 +92,17 @@
"blank_pass_modes"
:
{
"blank_pass_modes"
:
{
"S"
:
true
,
"S"
:
true
,
"M"
:
true
,
"M"
:
true
,
"T"
:
false
"T"
:
false
,
"OOR"
:
true
,
"TOR"
:
false
,
"OR"
:
true
,
"TR"
:
true
,
"CR"
:
false
,
"OOMR"
:
true
,
"TOMR"
:
false
,
"OMR"
:
true
,
"CMR"
:
false
,
"TMR"
:
true
},
},
"ready_time"
:
20
,
"ready_time"
:
20
,
"hang_timeout"
:
90
"hang_timeout"
:
90
...
@@ -137,9 +150,9 @@
...
@@ -137,9 +150,9 @@
},
},
"mycard"
:
{
"mycard"
:
{
"enabled"
:
false
,
"enabled"
:
false
,
"auth_base_url"
:
"https://
ygobbs.com
"
,
"auth_base_url"
:
"https://
sapi.moecube.com:444/accounts
"
,
"auth_database"
:
"postgres://233@233.mycard.moe/233"
,
"auth_database"
:
"postgres://233@233.mycard.moe/233"
,
"ban_get"
:
"https://
api.mycard.moe
/ygopro/big-brother/ban"
,
"ban_get"
:
"https://
sapi.moecube.com:444
/ygopro/big-brother/ban"
,
"auth_key"
:
"233333"
"auth_key"
:
"233333"
},
},
"challonge"
:
{
"challonge"
:
{
...
@@ -147,6 +160,7 @@
...
@@ -147,6 +160,7 @@
"post_detailed_score"
:
true
,
"post_detailed_score"
:
true
,
"post_score_midduel"
:
true
,
"post_score_midduel"
:
true
,
"cache_ttl"
:
60000
,
"cache_ttl"
:
60000
,
"no_match_mode"
:
false
,
"options"
:
{
"options"
:
{
"apiKey"
:
"123"
"apiKey"
:
"123"
},
},
...
@@ -157,13 +171,13 @@
...
@@ -157,13 +171,13 @@
"enabled"
:
false
,
"enabled"
:
false
,
"accesskey"
:
"233"
,
"accesskey"
:
"233"
,
"local"
:
"./deck_log/"
,
"local"
:
"./deck_log/"
,
"post"
:
"https://
api.mycard.moe
/ygopro/analytics/deck/text"
,
"post"
:
"https://
sapi.moecube.com:444
/ygopro/analytics/deck/text"
,
"arena"
:
"233"
"arena"
:
"233"
},
},
"big_brother"
:
{
"big_brother"
:
{
"enabled"
:
false
,
"enabled"
:
false
,
"accesskey"
:
"233"
,
"accesskey"
:
"233"
,
"post"
:
"https://
api.mycard.moe
/ygopro/big-brother"
"post"
:
"https://
sapi.moecube.com:444
/ygopro/big-brother"
},
},
"arena_mode"
:
{
"arena_mode"
:
{
"enabled"
:
false
,
"enabled"
:
false
,
...
@@ -171,13 +185,13 @@
...
@@ -171,13 +185,13 @@
"comment"
:
"mode: athletic / entertain"
,
"comment"
:
"mode: athletic / entertain"
,
"accesskey"
:
"233"
,
"accesskey"
:
"233"
,
"ready_time"
:
30
,
"ready_time"
:
30
,
"check_permit"
:
"https://
api.mycard.moe
/ygopro/match/permit"
,
"check_permit"
:
"https://
sapi.moecube.com:444
/ygopro/match/permit"
,
"post_score"
:
false
,
"post_score"
:
false
,
"get_score"
:
false
,
"get_score"
:
false
,
"punish_quit_before_match"
:
false
,
"punish_quit_before_match"
:
false
,
"init_post"
:
{
"init_post"
:
{
"enabled"
:
false
,
"enabled"
:
false
,
"url"
:
"https://
api.mycard.moe
/ygopro/match/clear"
,
"url"
:
"https://
sapi.moecube.com:444
/ygopro/match/clear"
,
"accesskey"
:
"momobako"
"accesskey"
:
"momobako"
}
}
},
},
...
@@ -197,8 +211,8 @@
...
@@ -197,8 +211,8 @@
},
},
"athletic_check"
:
{
"athletic_check"
:
{
"enabled"
:
false
,
"enabled"
:
false
,
"rankURL"
:
"https://
api.mycard.moe
/ygopro/analytics/deck/type"
,
"rankURL"
:
"https://
sapi.moecube.com:444
/ygopro/analytics/deck/type"
,
"identifierURL"
:
"https://
api.mycard.moe
/ygopro/identifier/production"
,
"identifierURL"
:
"https://
sapi.moecube.com:444
/ygopro/identifier/production"
,
"athleticFetchParams"
:
{
"athleticFetchParams"
:
{
"type"
:
"week"
,
"type"
:
"week"
,
"source"
:
"mycard-athletic"
"source"
:
"mycard-athletic"
...
...
roomlist.coffee
View file @
e6edd444
...
@@ -6,10 +6,10 @@ server = null
...
@@ -6,10 +6,10 @@ server = null
room_data
=
(
room
)
->
room_data
=
(
room
)
->
id
:
room
.
name
,
id
:
room
.
name
,
title
:
room
.
title
,
title
:
room
.
title
||
room
.
name
,
user
:
{
username
:
room
.
username
}
user
:
{
username
:
room
.
username
}
users
:
({
username
:
client
.
name
,
position
:
client
.
pos
}
for
client
in
room
.
players
),
users
:
({
username
:
client
.
name
,
position
:
client
.
pos
}
for
client
in
room
.
players
),
options
:
room
.
get_
old
_hostinfo
(),
# Should be updated when MyCard client updates
options
:
room
.
get_
roomlist
_hostinfo
(),
# Should be updated when MyCard client updates
arena
:
settings
.
modules
.
arena_mode
.
enabled
&&
room
.
arena
&&
settings
.
modules
.
arena_mode
.
mode
arena
:
settings
.
modules
.
arena_mode
.
enabled
&&
room
.
arena
&&
settings
.
modules
.
arena_mode
.
mode
init
=
(
http_server
,
ROOM_all
)
->
init
=
(
http_server
,
ROOM_all
)
->
...
...
roomlist.js
View file @
e6edd444
...
@@ -14,7 +14,7 @@
...
@@ -14,7 +14,7 @@
var
client
;
var
client
;
return
{
return
{
id
:
room
.
name
,
id
:
room
.
name
,
title
:
room
.
title
,
title
:
room
.
title
||
room
.
name
,
user
:
{
user
:
{
username
:
room
.
username
username
:
room
.
username
},
},
...
@@ -31,7 +31,7 @@
...
@@ -31,7 +31,7 @@
}
}
return
results
;
return
results
;
})(),
})(),
options
:
room
.
get_
old
_hostinfo
(),
// Should be updated when MyCard client updates
options
:
room
.
get_
roomlist
_hostinfo
(),
// Should be updated when MyCard client updates
arena
:
settings
.
modules
.
arena_mode
.
enabled
&&
room
.
arena
&&
settings
.
modules
.
arena_mode
.
mode
arena
:
settings
.
modules
.
arena_mode
.
enabled
&&
room
.
arena
&&
settings
.
modules
.
arena_mode
.
mode
};
};
};
};
...
...
ygopro-server.coffee
View file @
e6edd444
This diff is collapsed.
Click to expand it.
ygopro-server.js
View file @
e6edd444
This diff is collapsed.
Click to expand it.
ygopro.coffee
View file @
e6edd444
...
@@ -7,8 +7,20 @@ loadJSON = require('load-json-file').sync
...
@@ -7,8 +7,20 @@ loadJSON = require('load-json-file').sync
@
i18ns
=
loadJSON
'./data/i18n.json'
@
i18ns
=
loadJSON
'./data/i18n.json'
@
i18nR
=
{}
@
reloadI18nR
=
()
->
for
lang
,
data
of
@
i18ns
@
i18nR
[
lang
]
=
{}
for
key
,
text
of
data
@
i18nR
[
lang
][
key
]
=
{
regex
:
new
RegExp
(
"
\\
$
\\
{"
+
key
+
"
\\
}"
,
'g'
),
text
:
text
}
@
reloadI18nR
()
YGOProMessagesHelper
=
require
(
"./YGOProMessages.js"
).
YGOProMessagesHelper
# 为 SRVPro2 准备的库,这里拿这个库只用来测试,SRVPro1 对异步支持不是特别完善,因此不会有很多异步优化
YGOProMessagesHelper
=
require
(
"./YGOProMessages.js"
).
YGOProMessagesHelper
# 为 SRVPro2 准备的库,这里拿这个库只用来测试,SRVPro1 对异步支持不是特别完善,因此不会有很多异步优化
@
helper
=
new
YGOProMessagesHelper
()
@
helper
=
new
YGOProMessagesHelper
(
9000
)
@
structs
=
@
helper
.
structs
@
structs
=
@
helper
.
structs
@
structs_declaration
=
@
helper
.
structs_declaration
@
structs_declaration
=
@
helper
.
structs_declaration
...
@@ -54,9 +66,8 @@ translateHandler = (handler) ->
...
@@ -54,9 +66,8 @@ translateHandler = (handler) ->
for
line
in
_
.
lines
(
msg
)
for
line
in
_
.
lines
(
msg
)
if
player
>=
10
if
player
>=
10
line
=
"[Server]: "
+
line
line
=
"[Server]: "
+
line
for
o
,
r
of
@
i18ns
[
client
.
lang
]
for
o
,
r
of
@
i18nR
[
client
.
lang
]
re
=
new
RegExp
(
"
\\
$
\\
{"
+
o
+
"
\\
}"
,
'g'
)
line
=
line
.
replace
(
r
.
regex
,
r
.
text
)
line
=
line
.
replace
(
re
,
r
)
@
stoc_send
client
,
'CHAT'
,
{
@
stoc_send
client
,
'CHAT'
,
{
player
:
player
player
:
player
msg
:
line
msg
:
line
...
...
ygopro.js
View file @
e6edd444
...
@@ -14,9 +14,36 @@
...
@@ -14,9 +14,36 @@
this
.
i18ns
=
loadJSON
(
'
./data/i18n.json
'
);
this
.
i18ns
=
loadJSON
(
'
./data/i18n.json
'
);
this
.
i18nR
=
{};
this
.
reloadI18nR
=
function
()
{
var
data
,
key
,
lang
,
ref
,
results
,
text
;
ref
=
this
.
i18ns
;
results
=
[];
for
(
lang
in
ref
)
{
data
=
ref
[
lang
];
this
.
i18nR
[
lang
]
=
{};
results
.
push
((
function
()
{
var
results1
;
results1
=
[];
for
(
key
in
data
)
{
text
=
data
[
key
];
results1
.
push
(
this
.
i18nR
[
lang
][
key
]
=
{
regex
:
new
RegExp
(
"
\\
$
\\
{
"
+
key
+
"
\\
}
"
,
'
g
'
),
text
:
text
});
}
return
results1
;
}).
call
(
this
));
}
return
results
;
};
this
.
reloadI18nR
();
YGOProMessagesHelper
=
require
(
"
./YGOProMessages.js
"
).
YGOProMessagesHelper
;
// 为 SRVPro2 准备的库,这里拿这个库只用来测试,SRVPro1 对异步支持不是特别完善,因此不会有很多异步优化
YGOProMessagesHelper
=
require
(
"
./YGOProMessages.js
"
).
YGOProMessagesHelper
;
// 为 SRVPro2 准备的库,这里拿这个库只用来测试,SRVPro1 对异步支持不是特别完善,因此不会有很多异步优化
this
.
helper
=
new
YGOProMessagesHelper
();
this
.
helper
=
new
YGOProMessagesHelper
(
9000
);
this
.
structs
=
this
.
helper
.
structs
;
this
.
structs
=
this
.
helper
.
structs
;
...
@@ -69,7 +96,7 @@
...
@@ -69,7 +96,7 @@
//util
//util
this
.
stoc_send_chat
=
function
(
client
,
msg
,
player
=
8
)
{
this
.
stoc_send_chat
=
function
(
client
,
msg
,
player
=
8
)
{
var
i
,
len
,
line
,
o
,
r
,
re
,
re
f
,
ref1
;
var
i
,
len
,
line
,
o
,
r
,
ref
,
ref1
;
if
(
!
client
)
{
if
(
!
client
)
{
console
.
log
(
"
err stoc_send_chat
"
);
console
.
log
(
"
err stoc_send_chat
"
);
return
;
return
;
...
@@ -80,11 +107,10 @@
...
@@ -80,11 +107,10 @@
if
(
player
>=
10
)
{
if
(
player
>=
10
)
{
line
=
"
[Server]:
"
+
line
;
line
=
"
[Server]:
"
+
line
;
}
}
ref1
=
this
.
i18n
s
[
client
.
lang
];
ref1
=
this
.
i18n
R
[
client
.
lang
];
for
(
o
in
ref1
)
{
for
(
o
in
ref1
)
{
r
=
ref1
[
o
];
r
=
ref1
[
o
];
re
=
new
RegExp
(
"
\\
$
\\
{
"
+
o
+
"
\\
}
"
,
'
g
'
);
line
=
line
.
replace
(
r
.
regex
,
r
.
text
);
line
=
line
.
replace
(
re
,
r
);
}
}
this
.
stoc_send
(
client
,
'
CHAT
'
,
{
this
.
stoc_send
(
client
,
'
CHAT
'
,
{
player
:
player
,
player
:
player
,
...
...
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