├── data ├── long_resolve_cards.json ├── typedefs.json ├── pm2-docker-tournament.json ├── pm2-docker.json ├── proto_structs.json ├── default_data.json ├── structs.json ├── constants.json └── default_config.json ├── aragami-classes.ts ├── data-manager ├── entities │ ├── CreateAndUpdateTimeBase.ts │ ├── User.ts │ ├── BasePlayer.ts │ ├── Ban.ts │ ├── CloudReplayPlayer.ts │ ├── RandomDuelBan.ts │ ├── CreateAndUpdateTimeBase.js │ ├── CloudReplay.ts │ ├── User.js │ ├── BasePlayer.js │ ├── Ban.js │ ├── RandomDuelScore.ts │ ├── CloudReplayPlayer.js │ ├── RandomDuelBan.js │ ├── DuelLogPlayer.ts │ ├── DuelLog.ts │ ├── CloudReplay.js │ ├── RandomDuelScore.js │ ├── DuelLogPlayer.js │ └── DuelLog.js ├── DeckEncoder.ts └── DeckEncoder.js ├── restart.js ├── msg-polyfill ├── registry.js ├── registry.ts ├── base-polyfiller.ts ├── base-polyfiller.js ├── index.ts ├── index.js └── polyfillers │ ├── 0x1361.ts │ └── 0x1361.js ├── tsconfig.json ├── .gitignore ├── utility.ts ├── .dockerignore ├── utility.js ├── Dockerfile ├── .travis.yml ├── aragami-classes.js ├── package.json ├── Dockerfile.lite ├── roomlist.coffee ├── athletic-check.ts ├── athletic-check.js ├── README.md ├── .gitlab-ci.yml ├── roomlist.js ├── ygopro.coffee ├── challonge.js ├── ygopro-auth.coffee ├── challonge.ts ├── ygopro-auth.js ├── ygopro.js ├── ygopro-webhook.js ├── ygopro-deck-stats.js ├── struct.js ├── ygopro-update.js ├── YGOProMessages.ts ├── ygopro-tournament.js └── Replay.ts /data/long_resolve_cards.json: -------------------------------------------------------------------------------- 1 | [ 2 | 11110587, 3 | 32362575, 4 | 43040603, 5 | 58577036, 6 | 79106360 7 | ] 8 | -------------------------------------------------------------------------------- /data/typedefs.json: -------------------------------------------------------------------------------- 1 | { 2 | "unsigned int": "word32Ule", 3 | "unsigned short": "word16Ule", 4 | "unsigned char": "word8", 5 | "bool": "bool2" 6 | } -------------------------------------------------------------------------------- /aragami-classes.ts: -------------------------------------------------------------------------------- 1 | import { CacheKey, CacheTTL } from "aragami"; 2 | 3 | @CacheTTL(60000) 4 | export class ClientVersionBlocker { 5 | @CacheKey() 6 | clientKey: string; 7 | } 8 | -------------------------------------------------------------------------------- /data-manager/entities/CreateAndUpdateTimeBase.ts: -------------------------------------------------------------------------------- 1 | import {CreateDateColumn, UpdateDateColumn} from "typeorm"; 2 | 3 | export abstract class CreateAndUpdateTimeBase { 4 | @CreateDateColumn() 5 | createTime: Date; 6 | 7 | @UpdateDateColumn() 8 | updateTime: Date; 9 | } 10 | -------------------------------------------------------------------------------- /data/pm2-docker-tournament.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "ygopro-server", 5 | "script": "/ygopro-server/ygopro-server.js", 6 | "cwd": "/ygopro-server" 7 | }, 8 | { 9 | "name": "ygopro-tournament", 10 | "script": "/ygopro-server/ygopro-tournament.js", 11 | "cwd": "/ygopro-server" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /restart.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | var exec = require('child_process').exec; 3 | var check = function() { 4 | var now = moment(); 5 | if (now.hour() == 4 && now.minute() == 0) { 6 | console.log("It is time NOW!"); 7 | exec("pm2 restart all"); 8 | } 9 | else { 10 | console.log(now.format()); 11 | setTimeout(check, 10000); 12 | } 13 | } 14 | setTimeout(check, 60000); 15 | -------------------------------------------------------------------------------- /data-manager/entities/User.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, PrimaryColumn} from "typeorm"; 2 | import {CreateAndUpdateTimeBase} from "./CreateAndUpdateTimeBase"; 3 | 4 | @Entity() 5 | export class User extends CreateAndUpdateTimeBase { 6 | @PrimaryColumn({type: "varchar", length: 128}) 7 | key: string; 8 | 9 | @Column("varchar", {length: 16, nullable: true}) 10 | chatColor: string; 11 | } 12 | -------------------------------------------------------------------------------- /data/pm2-docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "ygopro-server", 5 | "script": "npm", 6 | "cwd": "/ygopro-server", 7 | "args": "start" 8 | }, 9 | { 10 | "name": "windbot", 11 | "script": "/ygopro-server/windbot/WindBot.exe", 12 | "cwd": "/ygopro-server/windbot/", 13 | "args": "ServerMode=true ServerPort=2399", 14 | "interpreter": "mono" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /msg-polyfill/registry.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.polyfillRegistry = void 0; 4 | const _0x1361_1 = require("./polyfillers/0x1361"); 5 | exports.polyfillRegistry = new Map(); 6 | const addPolyfiller = (version, polyfiller) => { 7 | exports.polyfillRegistry.set(version, polyfiller); 8 | }; 9 | addPolyfiller(0x1361, _0x1361_1.Polyfiller1361); 10 | -------------------------------------------------------------------------------- /msg-polyfill/registry.ts: -------------------------------------------------------------------------------- 1 | import { BasePolyfiller } from "./base-polyfiller"; 2 | import { Polyfiller1361 } from "./polyfillers/0x1361"; 3 | 4 | export const polyfillRegistry = new Map(); 5 | 6 | const addPolyfiller = (version: number, polyfiller: typeof BasePolyfiller) => { 7 | polyfillRegistry.set(version, polyfiller); 8 | } 9 | 10 | addPolyfiller(0x1361, Polyfiller1361); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "esModuleInterop": true, 6 | "resolveJsonModule": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true 9 | }, 10 | "compileOnSave": true, 11 | "allowJs": true, 12 | "include": [ 13 | "*.ts", 14 | "data-manager/*.ts", 15 | "data-manager/entities/*.ts", 16 | "msg-polyfill/*.ts", 17 | "msg-polyfill/polyfillers/*.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore 2 | jsconfig.json 3 | coffeelint.json 4 | .vscode/ 5 | .idea 6 | 7 | password.json 8 | config.*.json 9 | config.user.bak 10 | 11 | /bak 12 | /config 13 | /ygopro 14 | /windbot 15 | /decks 16 | /decks_save* 17 | /deck_log 18 | /replays 19 | /node_modules 20 | /ssl 21 | /ygosrv233 22 | /challonge 23 | /logs 24 | /plugins 25 | 26 | test* 27 | *.heapsnapshot 28 | *.tmp 29 | *.bak 30 | *.log 31 | log.* 32 | 33 | *.map 34 | 35 | Thumbs.db 36 | ehthumbs.db 37 | Desktop.ini 38 | $RECYCLE.BIN/ 39 | .DS_Store 40 | -------------------------------------------------------------------------------- /data-manager/entities/BasePlayer.ts: -------------------------------------------------------------------------------- 1 | import {Column, PrimaryGeneratedColumn} from "typeorm"; 2 | import {CreateAndUpdateTimeBase} from "./CreateAndUpdateTimeBase"; 3 | export abstract class BasePlayer extends CreateAndUpdateTimeBase { 4 | @PrimaryGeneratedColumn({unsigned: true, type: (global as any).PrimaryKeyType as ('bigint' | 'integer') || 'bigint'}) 5 | id: number; 6 | 7 | @Column({ type: "varchar", length: 20 }) 8 | name: string; 9 | 10 | @Column({ type: "tinyint" }) 11 | pos: number; 12 | } 13 | -------------------------------------------------------------------------------- /data-manager/DeckEncoder.ts: -------------------------------------------------------------------------------- 1 | import YGOProDeck from "ygopro-deck-encode"; 2 | 3 | export interface Deck { 4 | main: number[]; 5 | side: number[]; 6 | } 7 | 8 | // deprecated. Use YGOProDeck instead 9 | 10 | export function encodeDeck(deck: Deck) { 11 | const pdeck = new YGOProDeck(); 12 | pdeck.main = deck.main; 13 | pdeck.extra = []; 14 | pdeck.side = deck.side; 15 | return Buffer.from(pdeck.toUpdateDeckPayload()); 16 | } 17 | 18 | export function decodeDeck(buffer: Buffer): Deck { 19 | return YGOProDeck.fromUpdateDeckPayload(buffer); 20 | } 21 | -------------------------------------------------------------------------------- /data-manager/entities/Ban.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, PrimaryGeneratedColumn, Unique} from "typeorm"; 2 | import {CreateAndUpdateTimeBase} from "./CreateAndUpdateTimeBase"; 3 | 4 | @Entity() 5 | @Unique(["ip", "name"]) 6 | export class Ban extends CreateAndUpdateTimeBase { 7 | @PrimaryGeneratedColumn({ unsigned: true, type: (global as any).PrimaryKeyType as ('bigint' | 'integer') || 'bigint' }) 8 | id: number; 9 | 10 | @Index() 11 | @Column({ type: "varchar", length: 64, nullable: true }) 12 | ip: string; 13 | 14 | @Index() 15 | @Column({ type: "varchar", length: 20, nullable: true }) 16 | name: string; 17 | } 18 | -------------------------------------------------------------------------------- /utility.ts: -------------------------------------------------------------------------------- 1 | export async function retry( 2 | fn: () => Promise, 3 | count: number, 4 | delayFn: (attempt: number) => number = (attempt) => Math.pow(2, attempt) * 100 5 | ): Promise { 6 | let lastError: any; 7 | 8 | for (let attempt = 0; attempt < count; attempt++) { 9 | try { 10 | return await fn(); 11 | } catch (error) { 12 | lastError = error; 13 | if (attempt < count - 1) { 14 | const delay = delayFn(attempt); 15 | await new Promise((resolve) => setTimeout(resolve, delay)); 16 | } 17 | } 18 | } 19 | 20 | // 如果全部尝试失败,抛出最后一个错误 21 | throw lastError; 22 | } 23 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # ignore 2 | jsconfig.json 3 | coffeelint.json 4 | .vscode/ 5 | .idea 6 | 7 | password.json 8 | config.*.json 9 | config.user.bak 10 | 11 | /bak 12 | /config 13 | /ygopro 14 | /windbot 15 | /decks 16 | /decks_save* 17 | /deck_log 18 | /replays 19 | /node_modules 20 | /ssl 21 | /ygosrv233 22 | /challonge 23 | /logs 24 | /plugins 25 | 26 | test* 27 | *.heapsnapshot 28 | *.tmp 29 | *.bak 30 | *.log 31 | 32 | *.map 33 | 34 | Thumbs.db 35 | ehthumbs.db 36 | Desktop.ini 37 | $RECYCLE.BIN/ 38 | .DS_Store 39 | 40 | .git* 41 | .dockerignore 42 | .travis.yml 43 | Dockerfile* 44 | /docs 45 | /README.md 46 | /*.coffee 47 | /LICENSE 48 | -------------------------------------------------------------------------------- /utility.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.retry = retry; 4 | async function retry(fn, count, delayFn = (attempt) => Math.pow(2, attempt) * 100) { 5 | let lastError; 6 | for (let attempt = 0; attempt < count; attempt++) { 7 | try { 8 | return await fn(); 9 | } 10 | catch (error) { 11 | lastError = error; 12 | if (attempt < count - 1) { 13 | const delay = delayFn(attempt); 14 | await new Promise((resolve) => setTimeout(resolve, delay)); 15 | } 16 | } 17 | } 18 | // 如果全部尝试失败,抛出最后一个错误 19 | throw lastError; 20 | } 21 | -------------------------------------------------------------------------------- /data-manager/entities/CloudReplayPlayer.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, ManyToOne} from "typeorm"; 2 | import {CloudReplayPlayerInfo} from "../DataManager"; 3 | import {CloudReplay} from "./CloudReplay"; 4 | import {BasePlayer} from "./BasePlayer"; 5 | 6 | @Entity() 7 | export class CloudReplayPlayer extends BasePlayer { 8 | @Index() 9 | @Column({ type: "varchar", length: 128 }) 10 | key: string; 11 | 12 | @ManyToOne(() => CloudReplay, replay => replay.players) 13 | cloudReplay: CloudReplay; 14 | 15 | static fromPlayerInfo(info: CloudReplayPlayerInfo) { 16 | const p = new CloudReplayPlayer(); 17 | p.key = info.key; 18 | p.name = info.name; 19 | p.pos = info.pos; 20 | return p; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=git-registry.mycard.moe/mycard/srvpro:lite 2 | FROM $BASE_IMAGE 3 | LABEL Author="Nanahira " 4 | 5 | RUN apt update && \ 6 | apt -y install mono-complete && \ 7 | npm install -g pm2 && \ 8 | rm -rf /tmp/* /var/tmp/* /var/lib/apt/lists/* /var/log/* 9 | 10 | # windbot 11 | RUN git clone --depth=1 https://code.mycard.moe/mycard/windbot /tmp/windbot && \ 12 | cd /tmp/windbot && \ 13 | xbuild /property:Configuration=Release /property:TargetFrameworkVersion="v4.5" && \ 14 | mv /tmp/windbot/bin/Release /ygopro-server/windbot && \ 15 | cp -rf /ygopro-server/ygopro/cards.cdb /ygopro-server/windbot/ && \ 16 | rm -rf /tmp/windbot 17 | 18 | CMD [ "pm2-docker", "start", "/ygopro-server/data/pm2-docker.json" ] 19 | -------------------------------------------------------------------------------- /data-manager/entities/RandomDuelBan.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, PrimaryColumn} from "typeorm"; 2 | import {CreateAndUpdateTimeBase} from "./CreateAndUpdateTimeBase"; 3 | 4 | 5 | @Entity() 6 | export class RandomDuelBan extends CreateAndUpdateTimeBase { 7 | @PrimaryColumn({type: "varchar", length: 64}) 8 | ip: string; 9 | 10 | @Column("datetime") 11 | time: Date; 12 | 13 | @Column("smallint") 14 | count: number; 15 | 16 | @Column({type: "simple-array"}) 17 | reasons: string[] 18 | 19 | @Column({type: "tinyint", unsigned: true}) 20 | needTip: number; 21 | 22 | setNeedTip(need: boolean) { 23 | this.needTip = need ? 1 : 0; 24 | } 25 | 26 | getNeedTip() { 27 | return this.needTip > 0 ? true : false; 28 | } 29 | } -------------------------------------------------------------------------------- /data-manager/DeckEncoder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.encodeDeck = encodeDeck; 7 | exports.decodeDeck = decodeDeck; 8 | const ygopro_deck_encode_1 = __importDefault(require("ygopro-deck-encode")); 9 | // deprecated. Use YGOProDeck instead 10 | function encodeDeck(deck) { 11 | const pdeck = new ygopro_deck_encode_1.default(); 12 | pdeck.main = deck.main; 13 | pdeck.extra = []; 14 | pdeck.side = deck.side; 15 | return Buffer.from(pdeck.toUpdateDeckPayload()); 16 | } 17 | function decodeDeck(buffer) { 18 | return ygopro_deck_encode_1.default.fromUpdateDeckPayload(buffer); 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | dist: bionic 4 | script: 5 | # - echo "$DOCKER_PASSWORD" | docker login --username "$DOCKER_USERNAME" --password-stdin 6 | - docker build -t mycard/ygopro-server:latest . 7 | - mkdir output dist 8 | - docker run --rm -v "$PWD/output:/output" mycard/ygopro-server:latest -e 'require("child_process").execSync("cp -rf ./* /output")' 9 | - cd output && tar -zcf ../dist/ygopro-server.tar.gz --format=posix --exclude='.git*' ./* && cd .. 10 | # - docker push mycard/ygopro-server:latest 11 | #deploy: 12 | # provider: s3 13 | # access_key_id: ztDf0fCxr0eCgSIi 14 | # secret_access_key: 15 | # secure: Bm63Gi9Ok19pxhPiCbNnXlRYCmxxZWsj/PfxqGm1ruWLxinjJfpQHSRzSNTb/j0D8kK3uzUB+9MjrtcA5Nnfby4r+zuU+gvpW+3hOX2TiEtCSqYjenQmSsLD5WelMrBHXwfAQ5jOBzkdtC0vX4UTLAGcwJ8DhL9CbMJi0ZqDK9o= 16 | # bucket: mycard 17 | # skip_cleanup: true 18 | # local-dir: dist 19 | # upload-dir: srvpro 20 | # endpoint: https://minio.mycard.moe:9000 21 | branches: 22 | only: 23 | - master 24 | -------------------------------------------------------------------------------- /data/proto_structs.json: -------------------------------------------------------------------------------- 1 | { 2 | "CTOS":{ 3 | "RESPONSE": "", 4 | "HAND_RESULT": "CTOS_HandResult", 5 | "TP_RESULT": "CTOS_TPResult", 6 | "PLAYER_INFO": "CTOS_PlayerInfo", 7 | "JOIN_GAME":"CTOS_JoinGame", 8 | "HS_KICK":"CTOS_Kick", 9 | "UPDATE_DECK": "deck", 10 | "CHANGE_SIDE":"", 11 | "CHAT": "chat", 12 | "EXTERNAL_ADDRESS": "CTOS_ExternalAddress" 13 | }, 14 | "STOC":{ 15 | "JOIN_GAME":"STOC_JoinGame", 16 | "HS_WATCH_CHANGE":"STOC_HS_WatchChange", 17 | "TYPE_CHANGE":"STOC_TypeChange", 18 | "HS_PLAYER_CHANGE":"STOC_HS_PlayerChange", 19 | "HS_PLAYER_ENTER":"STOC_HS_PlayerEnter", 20 | "ERROR_MSG": "STOC_ErrorMsg", 21 | "GAME_MSG": "GameMsg_Hint_Card_only", 22 | "SELECT_HAND": "", 23 | "SELECT_TP": "", 24 | "HAND_RESULT": "STOC_HandResult", 25 | "REPLAY": "", 26 | "TIME_LIMIT": "STOC_TimeLimit", 27 | "CHAT": "STOC_Chat", 28 | "DECK_COUNT": "STOC_DeckCount" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /msg-polyfill/base-polyfiller.ts: -------------------------------------------------------------------------------- 1 | export class BasePolyfiller { 2 | 3 | async polyfillGameMsg(msgTitle: string, buffer: Buffer): Promise { 4 | return; 5 | } 6 | 7 | async polyfillResponse(msgTitle: string, buffer: Buffer): Promise { 8 | return; 9 | } 10 | 11 | splice(buf: Buffer, offset: number, deleteCount = 1): Buffer { 12 | if (offset < 0 || offset >= buf.length) return Buffer.alloc(0); 13 | 14 | deleteCount = Math.min(deleteCount, buf.length - offset); 15 | const end = offset + deleteCount; 16 | 17 | const newBuf = Buffer.concat([ 18 | buf.slice(0, offset), 19 | buf.slice(end) 20 | ]); 21 | 22 | return newBuf; 23 | } 24 | 25 | insert(buf: Buffer, offset: number, insertBuf: Buffer): Buffer { 26 | if (offset < 0) offset = 0; 27 | if (offset > buf.length) offset = buf.length; 28 | 29 | const newBuf = Buffer.concat([ 30 | buf.slice(0, offset), 31 | insertBuf, 32 | buf.slice(offset) 33 | ]); 34 | 35 | return newBuf; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /msg-polyfill/base-polyfiller.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.BasePolyfiller = void 0; 4 | class BasePolyfiller { 5 | async polyfillGameMsg(msgTitle, buffer) { 6 | return; 7 | } 8 | async polyfillResponse(msgTitle, buffer) { 9 | return; 10 | } 11 | splice(buf, offset, deleteCount = 1) { 12 | if (offset < 0 || offset >= buf.length) 13 | return Buffer.alloc(0); 14 | deleteCount = Math.min(deleteCount, buf.length - offset); 15 | const end = offset + deleteCount; 16 | const newBuf = Buffer.concat([ 17 | buf.slice(0, offset), 18 | buf.slice(end) 19 | ]); 20 | return newBuf; 21 | } 22 | insert(buf, offset, insertBuf) { 23 | if (offset < 0) 24 | offset = 0; 25 | if (offset > buf.length) 26 | offset = buf.length; 27 | const newBuf = Buffer.concat([ 28 | buf.slice(0, offset), 29 | insertBuf, 30 | buf.slice(offset) 31 | ]); 32 | return newBuf; 33 | } 34 | } 35 | exports.BasePolyfiller = BasePolyfiller; 36 | -------------------------------------------------------------------------------- /aragami-classes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.ClientVersionBlocker = void 0; 13 | const aragami_1 = require("aragami"); 14 | let ClientVersionBlocker = class ClientVersionBlocker { 15 | }; 16 | exports.ClientVersionBlocker = ClientVersionBlocker; 17 | __decorate([ 18 | (0, aragami_1.CacheKey)(), 19 | __metadata("design:type", String) 20 | ], ClientVersionBlocker.prototype, "clientKey", void 0); 21 | exports.ClientVersionBlocker = ClientVersionBlocker = __decorate([ 22 | (0, aragami_1.CacheTTL)(60000) 23 | ], ClientVersionBlocker); 24 | -------------------------------------------------------------------------------- /data-manager/entities/CreateAndUpdateTimeBase.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.CreateAndUpdateTimeBase = void 0; 13 | const typeorm_1 = require("typeorm"); 14 | class CreateAndUpdateTimeBase { 15 | } 16 | exports.CreateAndUpdateTimeBase = CreateAndUpdateTimeBase; 17 | __decorate([ 18 | (0, typeorm_1.CreateDateColumn)(), 19 | __metadata("design:type", Date) 20 | ], CreateAndUpdateTimeBase.prototype, "createTime", void 0); 21 | __decorate([ 22 | (0, typeorm_1.UpdateDateColumn)(), 23 | __metadata("design:type", Date) 24 | ], CreateAndUpdateTimeBase.prototype, "updateTime", void 0); 25 | -------------------------------------------------------------------------------- /msg-polyfill/index.ts: -------------------------------------------------------------------------------- 1 | import { BasePolyfiller } from "./base-polyfiller"; 2 | import { polyfillRegistry } from "./registry"; 3 | 4 | const getPolyfillers = (version: number) => { 5 | const polyfillers: {version: number, polyfiller: BasePolyfiller}[] = []; 6 | for (const [pVersion, polyfillerCls] of polyfillRegistry.entries()) { 7 | if (version <= pVersion) { 8 | polyfillers.push({ version: pVersion, polyfiller: new polyfillerCls() }); 9 | } 10 | } 11 | polyfillers.sort((a, b) => a.version - b.version); 12 | return polyfillers.map(p => p.polyfiller); 13 | } 14 | 15 | 16 | export async function polyfillGameMsg(version: number, msgTitle: string, buffer: Buffer) { 17 | const polyfillers = getPolyfillers(version); 18 | let pbuf = buffer; 19 | for (const polyfiller of polyfillers) { 20 | const newBuf = await polyfiller.polyfillGameMsg(msgTitle, pbuf); 21 | if (newBuf) { 22 | pbuf = newBuf; 23 | } 24 | } 25 | if (pbuf === buffer) { 26 | return undefined; 27 | } else if (pbuf.length <= buffer.length) { 28 | pbuf.copy(buffer, 0, 0, pbuf.length); 29 | return pbuf.length === buffer.length 30 | ? undefined 31 | : buffer.slice(0, pbuf.length); 32 | } else { 33 | return pbuf; 34 | } 35 | } 36 | 37 | export async function polyfillResponse(version: number, msgTitle: string, buffer: Buffer) { 38 | const polyfillers = getPolyfillers(version); 39 | for (const polyfiller of polyfillers) { 40 | await polyfiller.polyfillResponse(msgTitle, buffer); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /data-manager/entities/CloudReplay.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, OneToMany, PrimaryColumn} from "typeorm"; 2 | import {CloudReplayPlayer} from "./CloudReplayPlayer"; 3 | import _ from "underscore"; 4 | import moment from "moment"; 5 | import {CreateAndUpdateTimeBase} from "./CreateAndUpdateTimeBase"; 6 | 7 | @Entity({ 8 | orderBy: { 9 | date: "DESC" 10 | } 11 | }) 12 | export class CloudReplay extends CreateAndUpdateTimeBase { 13 | @PrimaryColumn({ unsigned: true, type: (global as any).PrimaryKeyType as ('bigint' | 'integer') || 'bigint' }) 14 | id: number; 15 | 16 | @Column({ type: "text" }) 17 | data: string; 18 | 19 | fromBuffer(buffer: Buffer) { 20 | this.data = buffer.toString("base64"); 21 | } 22 | 23 | toBuffer() { 24 | return Buffer.from(this.data, "base64"); 25 | } 26 | 27 | @Index() 28 | @Column({ type: "datetime" }) 29 | date: Date; 30 | 31 | getDateString() { 32 | return moment(this.date).format('YYYY-MM-DD HH:mm:ss') 33 | } 34 | 35 | @OneToMany(() => CloudReplayPlayer, player => player.cloudReplay) 36 | players: CloudReplayPlayer[]; 37 | 38 | getPlayerNamesString() { 39 | const playerInfos = _.clone(this.players); 40 | playerInfos.sort((p1, p2) => p1.pos - p2.pos); 41 | return playerInfos[0].name + (playerInfos[2] ? "+" + playerInfos[2].name : "") + " VS " + (playerInfos[1] ? playerInfos[1].name : "AI") + (playerInfos[3] ? "+" + playerInfos[3].name : ""); 42 | } 43 | 44 | getDisplayString() { 45 | return `R#${this.id} ${this.getPlayerNamesString()} ${this.getDateString()}`; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /data-manager/entities/User.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.User = void 0; 13 | const typeorm_1 = require("typeorm"); 14 | const CreateAndUpdateTimeBase_1 = require("./CreateAndUpdateTimeBase"); 15 | let User = class User extends CreateAndUpdateTimeBase_1.CreateAndUpdateTimeBase { 16 | }; 17 | exports.User = User; 18 | __decorate([ 19 | (0, typeorm_1.PrimaryColumn)({ type: "varchar", length: 128 }), 20 | __metadata("design:type", String) 21 | ], User.prototype, "key", void 0); 22 | __decorate([ 23 | (0, typeorm_1.Column)("varchar", { length: 16, nullable: true }), 24 | __metadata("design:type", String) 25 | ], User.prototype, "chatColor", void 0); 26 | exports.User = User = __decorate([ 27 | (0, typeorm_1.Entity)() 28 | ], User); 29 | -------------------------------------------------------------------------------- /msg-polyfill/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.polyfillGameMsg = polyfillGameMsg; 4 | exports.polyfillResponse = polyfillResponse; 5 | const registry_1 = require("./registry"); 6 | const getPolyfillers = (version) => { 7 | const polyfillers = []; 8 | for (const [pVersion, polyfillerCls] of registry_1.polyfillRegistry.entries()) { 9 | if (version <= pVersion) { 10 | polyfillers.push({ version: pVersion, polyfiller: new polyfillerCls() }); 11 | } 12 | } 13 | polyfillers.sort((a, b) => a.version - b.version); 14 | return polyfillers.map(p => p.polyfiller); 15 | }; 16 | async function polyfillGameMsg(version, msgTitle, buffer) { 17 | const polyfillers = getPolyfillers(version); 18 | let pbuf = buffer; 19 | for (const polyfiller of polyfillers) { 20 | const newBuf = await polyfiller.polyfillGameMsg(msgTitle, pbuf); 21 | if (newBuf) { 22 | pbuf = newBuf; 23 | } 24 | } 25 | if (pbuf === buffer) { 26 | return undefined; 27 | } 28 | else if (pbuf.length <= buffer.length) { 29 | pbuf.copy(buffer, 0, 0, pbuf.length); 30 | return pbuf.length === buffer.length 31 | ? undefined 32 | : buffer.slice(0, pbuf.length); 33 | } 34 | else { 35 | return pbuf; 36 | } 37 | } 38 | async function polyfillResponse(version, msgTitle, buffer) { 39 | const polyfillers = getPolyfillers(version); 40 | for (const polyfiller of polyfillers) { 41 | await polyfiller.polyfillResponse(msgTitle, buffer); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /data-manager/entities/BasePlayer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.BasePlayer = void 0; 13 | const typeorm_1 = require("typeorm"); 14 | const CreateAndUpdateTimeBase_1 = require("./CreateAndUpdateTimeBase"); 15 | class BasePlayer extends CreateAndUpdateTimeBase_1.CreateAndUpdateTimeBase { 16 | } 17 | exports.BasePlayer = BasePlayer; 18 | __decorate([ 19 | (0, typeorm_1.PrimaryGeneratedColumn)({ unsigned: true, type: global.PrimaryKeyType || 'bigint' }), 20 | __metadata("design:type", Number) 21 | ], BasePlayer.prototype, "id", void 0); 22 | __decorate([ 23 | (0, typeorm_1.Column)({ type: "varchar", length: 20 }), 24 | __metadata("design:type", String) 25 | ], BasePlayer.prototype, "name", void 0); 26 | __decorate([ 27 | (0, typeorm_1.Column)({ type: "tinyint" }), 28 | __metadata("design:type", Number) 29 | ], BasePlayer.prototype, "pos", void 0); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srvpro", 3 | "version": "2.3.3", 4 | "description": "a server for ygopro", 5 | "repository": "github:mycard/srvpro", 6 | "keywords": [ 7 | "mycard", 8 | "ygopro", 9 | "srvpro", 10 | "server" 11 | ], 12 | "author": "zh99998 , mercury233 , Nanahira <78877@qq.com>", 13 | "dependencies": { 14 | "aragami": "^1.2.5", 15 | "async": "^3.2.0", 16 | "axios": "^0.19.2", 17 | "bunyan": "^1.8.14", 18 | "deepmerge": "^4.2.2", 19 | "formidable": "^1.2.6", 20 | "geoip-country-lite": "^1.0.0", 21 | "ip6addr": "^0.2.5", 22 | "jszip": "^3.5.0", 23 | "load-json-file": "^6.2.0", 24 | "lzma": "^2.3.2", 25 | "moment": "^2.29.1", 26 | "mysql": "^2.18.1", 27 | "node-os-utils": "^1.3.2", 28 | "p-queue": "^6.6.2", 29 | "pg": "^6.4.2", 30 | "q": "^1.5.1", 31 | "querystring": "^0.2.0", 32 | "reflect-metadata": "^0.1.13", 33 | "request": "^2.88.2", 34 | "sqlite3": "^5.0.2", 35 | "typeorm": "^0.2.29", 36 | "underscore": "^1.11.0", 37 | "underscore.string": "^3.3.6", 38 | "ws": "^8.9.0", 39 | "ygopro-deck-encode": "^1.0.14" 40 | }, 41 | "license": "AGPL-3.0", 42 | "scripts": { 43 | "build": "coffee -c *.coffee && tsc", 44 | "start": "node ygopro-server.js | bunyan", 45 | "tournament": "node ygopro-tournament.js", 46 | "pre": "node ygopro-pre.js", 47 | "updated": "node ygopro-update.js", 48 | "webhook": "node ygopro-webhook.js" 49 | }, 50 | "devDependencies": { 51 | "@types/bunyan": "^1.8.8", 52 | "@types/ip6addr": "^0.2.3", 53 | "@types/lzma": "^2.3.0", 54 | "@types/node": "^16.18.126", 55 | "@types/underscore": "^1.11.4", 56 | "@types/ws": "^8.5.3", 57 | "coffeescript": "^2.7.0", 58 | "typescript": "^5.8.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /data-manager/entities/Ban.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.Ban = void 0; 13 | const typeorm_1 = require("typeorm"); 14 | const CreateAndUpdateTimeBase_1 = require("./CreateAndUpdateTimeBase"); 15 | let Ban = class Ban extends CreateAndUpdateTimeBase_1.CreateAndUpdateTimeBase { 16 | }; 17 | exports.Ban = Ban; 18 | __decorate([ 19 | (0, typeorm_1.PrimaryGeneratedColumn)({ unsigned: true, type: global.PrimaryKeyType || 'bigint' }), 20 | __metadata("design:type", Number) 21 | ], Ban.prototype, "id", void 0); 22 | __decorate([ 23 | (0, typeorm_1.Index)(), 24 | (0, typeorm_1.Column)({ type: "varchar", length: 64, nullable: true }), 25 | __metadata("design:type", String) 26 | ], Ban.prototype, "ip", void 0); 27 | __decorate([ 28 | (0, typeorm_1.Index)(), 29 | (0, typeorm_1.Column)({ type: "varchar", length: 20, nullable: true }), 30 | __metadata("design:type", String) 31 | ], Ban.prototype, "name", void 0); 32 | exports.Ban = Ban = __decorate([ 33 | (0, typeorm_1.Entity)(), 34 | (0, typeorm_1.Unique)(["ip", "name"]) 35 | ], Ban); 36 | -------------------------------------------------------------------------------- /data/default_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "badwords": { 3 | "file": "./config/badwords.json", 4 | "level0": ["滚", "衮", "操", "草", "艹", "狗", "日", "曰", "妈", "娘", "逼"], 5 | "level1": ["傻逼", "鸡巴"], 6 | "level2": ["死妈", "草你妈"], 7 | "level3": ["迷奸", "仿真枪"] 8 | }, 9 | "tips": { 10 | "file": "./config/tips.json", 11 | "tips": [ 12 | "欢迎来到本服务器", 13 | "本服务器使用萌卡代码搭建" 14 | ] 15 | }, 16 | "dialogues": { 17 | "file": "./config/dialogues.json", 18 | "dialogues": { 19 | "46986414": [ 20 | "出来吧,我最强的仆人,黑魔导!" 21 | ], 22 | "58481572": [ 23 | "我们来做朋友吧!" 24 | ] 25 | } 26 | }, 27 | "users": { 28 | "file": "./config/admin_user.json", 29 | "permission_examples": { 30 | "sudo": { 31 | "get_rooms": true, 32 | "duel_log": true, 33 | "download_replay": true, 34 | "clear_duel_log": true, 35 | "deck_dashboard_read": true, 36 | "deck_dashboard_write": true, 37 | "shout": true, 38 | "stop": true, 39 | "change_settings": true, 40 | "ban_user": true, 41 | "kick_user": true, 42 | "start_death": true, 43 | "pre_dashboard": true, 44 | "update_dashboard": true 45 | }, 46 | "judge": { 47 | "get_rooms": true, 48 | "duel_log": true, 49 | "download_replay": true, 50 | "deck_dashboard_read": true, 51 | "deck_dashboard_write": true, 52 | "shout": true, 53 | "kick_user": true, 54 | "start_death": true 55 | }, 56 | "streamer": { 57 | "get_rooms": true, 58 | "duel_log": true, 59 | "download_replay": true, 60 | "deck_dashboard_read": true 61 | } 62 | }, 63 | "users": { 64 | "root": { 65 | "password": "123456", 66 | "enabled": false, 67 | "permissions": "sudo" 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Dockerfile.lite: -------------------------------------------------------------------------------- 1 | # Dockerfile for SRVPro Lite 2 | FROM debian:bullseye as premake-builder 3 | 4 | RUN apt update && \ 5 | env DEBIAN_FRONTEND=noninteractive apt install -y wget build-essential p7zip-full uuid-dev && \ 6 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/* 7 | 8 | WORKDIR /usr/src 9 | RUN wget -O premake.zip https://github.com/premake/premake-core/releases/download/v5.0.0-beta7/premake-5.0.0-beta7-src.zip && \ 10 | 7z x -y -opremake premake.zip && \ 11 | cd premake/build/gmake.unix && \ 12 | make -j$(nproc) 13 | 14 | FROM node:16-bullseye-slim 15 | LABEL Author="Nanahira " 16 | 17 | # apt 18 | RUN apt update && \ 19 | env DEBIAN_FRONTEND=noninteractive apt install -y wget git build-essential libevent-dev libsqlite3-dev p7zip-full python3 python-is-python3 liblua5.3-dev && \ 20 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 21 | 22 | # srvpro 23 | COPY . /ygopro-server 24 | WORKDIR /ygopro-server 25 | RUN npm ci && \ 26 | mkdir config decks replays logs 27 | 28 | COPY --from=premake-builder /usr/src/premake/bin/release/premake5 /usr/bin/premake5 29 | 30 | RUN git clone --branch=server --recursive --depth=1 https://code.mycard.moe/mycard/ygopro && \ 31 | cd ygopro && \ 32 | git submodule foreach git checkout master && \ 33 | premake5 gmake --lua-deb && \ 34 | cd build && \ 35 | make config=release -j$(nproc) && \ 36 | cd .. && \ 37 | mv ./bin/release/ygopro . && \ 38 | strip ygopro && \ 39 | mkdir replay expansions && \ 40 | 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 && \ 41 | ls gframe | sed '/config.h/d' | xargs -I {} rm -rf gframe/{} 42 | 43 | # infos 44 | WORKDIR /ygopro-server 45 | EXPOSE 7911 7922 7933 46 | # VOLUME [ /ygopro-server/config, /ygopro-server/decks, /ygopro-server/replays ] 47 | 48 | CMD [ "npm", "start" ] 49 | -------------------------------------------------------------------------------- /data-manager/entities/RandomDuelScore.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, PrimaryColumn} from "typeorm"; 2 | import {CreateAndUpdateTimeBase} from "./CreateAndUpdateTimeBase"; 3 | 4 | @Entity() 5 | export class RandomDuelScore extends CreateAndUpdateTimeBase { 6 | @PrimaryColumn({type: "varchar", length: 20}) 7 | name: string; 8 | 9 | @Index() 10 | @Column("int", {unsigned: true, default: 0}) 11 | winCount: number; 12 | 13 | @Index() 14 | @Column("int", {unsigned: true, default: 0}) 15 | loseCount: number; 16 | 17 | @Index() 18 | @Column("int", {unsigned: true, default: 0}) 19 | fleeCount: number; 20 | 21 | @Column("int", {unsigned: true, default: 0}) 22 | winCombo: number; 23 | 24 | getDisplayName() { 25 | return this.name.split("$")[0]; 26 | } 27 | 28 | win() { 29 | ++this.winCount; 30 | ++this.winCombo; 31 | } 32 | 33 | lose() { 34 | ++this.loseCount; 35 | this.winCombo = 0; 36 | } 37 | 38 | flee() { 39 | ++this.fleeCount; 40 | this.lose(); 41 | } 42 | 43 | getScoreText(displayName: string) { 44 | const total = this.winCount + this.loseCount; 45 | if (this.winCount < 2 && total < 3) { 46 | return `${displayName} \${random_score_not_enough}`; 47 | } 48 | if (this.winCombo >= 2) { 49 | return `\${random_score_part1}${displayName} \${random_score_part2} ${Math.ceil(this.winCount / total * 100)}\${random_score_part3} ${Math.ceil(this.fleeCount / total * 100)}\${random_score_part4_combo}${this.winCombo}\${random_score_part5_combo}`; 50 | } else { 51 | //return displayName + " 的今日战绩:胜率" + Math.ceil(this.winCount/total*100) + "%,逃跑率" + Math.ceil(this.fleeCount/total*100) + "%," + this.winCombo + "连胜中!" 52 | return `\${random_score_part1}${displayName} \${random_score_part2} ${Math.ceil(this.winCount / total * 100)}\${random_score_part3} ${Math.ceil(this.fleeCount / total * 100)}\${random_score_part4}`; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /data-manager/entities/CloudReplayPlayer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var CloudReplayPlayer_1; 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | exports.CloudReplayPlayer = void 0; 14 | const typeorm_1 = require("typeorm"); 15 | const CloudReplay_1 = require("./CloudReplay"); 16 | const BasePlayer_1 = require("./BasePlayer"); 17 | let CloudReplayPlayer = CloudReplayPlayer_1 = class CloudReplayPlayer extends BasePlayer_1.BasePlayer { 18 | static fromPlayerInfo(info) { 19 | const p = new CloudReplayPlayer_1(); 20 | p.key = info.key; 21 | p.name = info.name; 22 | p.pos = info.pos; 23 | return p; 24 | } 25 | }; 26 | exports.CloudReplayPlayer = CloudReplayPlayer; 27 | __decorate([ 28 | (0, typeorm_1.Index)(), 29 | (0, typeorm_1.Column)({ type: "varchar", length: 128 }), 30 | __metadata("design:type", String) 31 | ], CloudReplayPlayer.prototype, "key", void 0); 32 | __decorate([ 33 | (0, typeorm_1.ManyToOne)(() => CloudReplay_1.CloudReplay, replay => replay.players), 34 | __metadata("design:type", CloudReplay_1.CloudReplay) 35 | ], CloudReplayPlayer.prototype, "cloudReplay", void 0); 36 | exports.CloudReplayPlayer = CloudReplayPlayer = CloudReplayPlayer_1 = __decorate([ 37 | (0, typeorm_1.Entity)() 38 | ], CloudReplayPlayer); 39 | -------------------------------------------------------------------------------- /roomlist.coffee: -------------------------------------------------------------------------------- 1 | WebSocketServer = require('ws').Server 2 | url = require('url') 3 | settings = global.settings 4 | 5 | server = null 6 | 7 | room_data = (room)-> 8 | id: room.name, 9 | title: room.title || room.name, 10 | user: {username: room.username} 11 | users: ({username: client.name, position: client.pos} for client in room.players), 12 | options: room.get_roomlist_hostinfo(), # Should be updated when MyCard client updates 13 | arena: settings.modules.arena_mode.enabled && room.arena && settings.modules.arena_mode.mode 14 | 15 | clients = new Set() 16 | 17 | init = (http_server, ROOM_all)-> 18 | server = new WebSocketServer 19 | server: http_server 20 | 21 | server.on 'connection', (connection, upgradeReq) -> 22 | connection.filter = url.parse(upgradeReq.url, true).query.filter || 'waiting' 23 | connection.send JSON.stringify 24 | event: 'init' 25 | data: room_data(room) for room in ROOM_all when room and room.established and (connection.filter == 'started' or !room.private) and ((room.duel_stage != 0) == (connection.filter == 'started')) 26 | clients.add connection 27 | connection.on('close', () -> clients.delete connection if clients.has connection) 28 | 29 | create = (room)-> 30 | broadcast('create', room_data(room), 'waiting') if !room.private 31 | 32 | update = (room)-> 33 | broadcast('update', room_data(room), 'waiting') if !room.private 34 | 35 | start = (room)-> 36 | broadcast('delete', room.name, 'waiting') if !room.private 37 | broadcast('create', room_data(room), 'started') 38 | 39 | _delete = (room)-> 40 | if(room.duel_stage != 0) 41 | broadcast('delete', room.name, 'started') 42 | else 43 | broadcast('delete', room.name, 'waiting') if !room.private 44 | 45 | broadcast = (event, data, filter)-> 46 | return if !server 47 | message = JSON.stringify 48 | event: event 49 | data: data 50 | for connection in Array.from(clients.values()) when connection.filter == filter 51 | try 52 | connection.send message 53 | 54 | module.exports = 55 | init: init 56 | create: create 57 | update: update 58 | start: start 59 | delete: _delete 60 | -------------------------------------------------------------------------------- /data-manager/entities/RandomDuelBan.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.RandomDuelBan = void 0; 13 | const typeorm_1 = require("typeorm"); 14 | const CreateAndUpdateTimeBase_1 = require("./CreateAndUpdateTimeBase"); 15 | let RandomDuelBan = class RandomDuelBan extends CreateAndUpdateTimeBase_1.CreateAndUpdateTimeBase { 16 | setNeedTip(need) { 17 | this.needTip = need ? 1 : 0; 18 | } 19 | getNeedTip() { 20 | return this.needTip > 0 ? true : false; 21 | } 22 | }; 23 | exports.RandomDuelBan = RandomDuelBan; 24 | __decorate([ 25 | (0, typeorm_1.PrimaryColumn)({ type: "varchar", length: 64 }), 26 | __metadata("design:type", String) 27 | ], RandomDuelBan.prototype, "ip", void 0); 28 | __decorate([ 29 | (0, typeorm_1.Column)("datetime"), 30 | __metadata("design:type", Date) 31 | ], RandomDuelBan.prototype, "time", void 0); 32 | __decorate([ 33 | (0, typeorm_1.Column)("smallint"), 34 | __metadata("design:type", Number) 35 | ], RandomDuelBan.prototype, "count", void 0); 36 | __decorate([ 37 | (0, typeorm_1.Column)({ type: "simple-array" }), 38 | __metadata("design:type", Array) 39 | ], RandomDuelBan.prototype, "reasons", void 0); 40 | __decorate([ 41 | (0, typeorm_1.Column)({ type: "tinyint", unsigned: true }), 42 | __metadata("design:type", Number) 43 | ], RandomDuelBan.prototype, "needTip", void 0); 44 | exports.RandomDuelBan = RandomDuelBan = __decorate([ 45 | (0, typeorm_1.Entity)() 46 | ], RandomDuelBan); 47 | -------------------------------------------------------------------------------- /athletic-check.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import qs from "querystring"; 3 | import moment, {Moment} from "moment"; 4 | 5 | interface Deck { 6 | main: number[]; 7 | side: number[]; 8 | } 9 | 10 | interface Config { 11 | rankURL: string; 12 | identifierURL: string; 13 | athleticFetchParams: any; 14 | rankCount: number; 15 | ttl: number; 16 | } 17 | 18 | interface AthleticDecksReturnData { 19 | name: string 20 | } 21 | 22 | interface ReturnMessage { 23 | success: boolean; 24 | athletic?: number; 25 | message: string; 26 | } 27 | 28 | export class AthleticChecker { 29 | config: Config; 30 | athleticDeckCache: string[]; 31 | lastAthleticDeckFetchTime: Moment; 32 | constructor(config: Config) { 33 | this.config = config; 34 | } 35 | deckToString(deck: Deck) { 36 | const deckText = '#ygopro-server deck log\n#main\n' + deck.main.join('\n') + '\n!side\n' + deck.side.join('\n') + '\n'; 37 | return deckText; 38 | } 39 | async getAthleticDecks(): Promise { 40 | if (this.athleticDeckCache && moment().diff(this.lastAthleticDeckFetchTime, "seconds") < this.config.ttl) { 41 | return this.athleticDeckCache; 42 | } 43 | const { data } = await axios.get(this.config.rankURL, { 44 | timeout: 10000, 45 | responseType: "json", 46 | paramsSerializer: qs.stringify, 47 | params: this.config.athleticFetchParams 48 | }); 49 | const athleticDecks = (data as AthleticDecksReturnData[]).slice(0, this.config.rankCount).map(m => m.name); 50 | this.athleticDeckCache = athleticDecks; 51 | this.lastAthleticDeckFetchTime = moment(); 52 | return athleticDecks; 53 | } 54 | async getDeckType(deck: Deck): Promise { 55 | const deckString = this.deckToString(deck); 56 | const { data } = await axios.post(this.config.identifierURL, qs.stringify({ deck: deckString }), { 57 | timeout: 10000, 58 | responseType: "json" 59 | }); 60 | return data.deck; 61 | } 62 | async checkAthletic(deck: Deck): Promise { 63 | try { 64 | const deckType = await this.getDeckType(deck); 65 | if (deckType === '迷之卡组') { 66 | return { success: true, athletic: 0, message: null }; 67 | } 68 | const athleticDecks = await this.getAthleticDecks(); 69 | const athletic = athleticDecks.findIndex(d => d === deckType) + 1; 70 | return { success: true, athletic, message: null }; 71 | } catch (e) { 72 | return { success: false, message: e.toString() }; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /data-manager/entities/DuelLogPlayer.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, ManyToOne} from "typeorm"; 2 | import {BasePlayer} from "./BasePlayer"; 3 | import {DuelLog} from "./DuelLog"; 4 | import {Deck, decodeDeck, encodeDeck} from "../DeckEncoder"; 5 | import {DuelLogPlayerInfo} from "../DataManager"; 6 | 7 | @Entity() 8 | export class DuelLogPlayer extends BasePlayer { 9 | @Index() 10 | @Column({ type: "varchar", length: 20 }) 11 | realName: string; 12 | 13 | @Column({ type: "varchar", length: 64, nullable: true }) 14 | ip: string; 15 | 16 | @Column("tinyint", {unsigned: true}) 17 | isFirst: number; 18 | 19 | @Index() 20 | @Column("tinyint") 21 | score: number; 22 | 23 | @Column("int", {nullable: true}) 24 | lp: number; 25 | 26 | @Column("smallint", {nullable: true}) 27 | cardCount: number; 28 | 29 | @Column("text", {nullable: true}) 30 | startDeckBuffer: string; 31 | 32 | @Column("text", {nullable: true}) 33 | currentDeckBuffer: string; 34 | 35 | @Column("tinyint") 36 | winner: number; 37 | 38 | setStartDeck(deck: Deck) { 39 | if(!deck) { 40 | this.startDeckBuffer = null; 41 | return; 42 | } 43 | this.startDeckBuffer = encodeDeck(deck).toString("base64"); 44 | } 45 | 46 | getStartDeck() { 47 | return decodeDeck(Buffer.from(this.startDeckBuffer, "base64")); 48 | } 49 | 50 | setCurrentDeck(deck: Deck) { 51 | if(!deck) { 52 | this.currentDeckBuffer = null; 53 | return; 54 | } 55 | this.currentDeckBuffer = encodeDeck(deck).toString("base64"); 56 | } 57 | 58 | getCurrentDeck() { 59 | return decodeDeck(Buffer.from(this.currentDeckBuffer, "base64")); 60 | } 61 | 62 | @ManyToOne(() => DuelLog, duelLog => duelLog.players) 63 | duelLog: DuelLog; 64 | 65 | static fromDuelLogPlayerInfo(info: DuelLogPlayerInfo) { 66 | const p = new DuelLogPlayer(); 67 | p.name = info.name; 68 | p.pos = info.pos; 69 | p.realName = info.realName; 70 | p.lp = info.lp; 71 | p.ip = info.ip; 72 | p.score = info.score; 73 | p.cardCount = info.cardCount; 74 | p.isFirst = info.isFirst ? 1 : 0; 75 | p.winner = info.winner ? 1 : 0; 76 | p.startDeckBuffer = info.startDeckBuffer?.toString("base64") || null; 77 | p.setCurrentDeck(info.deck); 78 | return p; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /athletic-check.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.AthleticChecker = void 0; 7 | const axios_1 = __importDefault(require("axios")); 8 | const querystring_1 = __importDefault(require("querystring")); 9 | const moment_1 = __importDefault(require("moment")); 10 | class AthleticChecker { 11 | constructor(config) { 12 | this.config = config; 13 | } 14 | deckToString(deck) { 15 | const deckText = '#ygopro-server deck log\n#main\n' + deck.main.join('\n') + '\n!side\n' + deck.side.join('\n') + '\n'; 16 | return deckText; 17 | } 18 | async getAthleticDecks() { 19 | if (this.athleticDeckCache && (0, moment_1.default)().diff(this.lastAthleticDeckFetchTime, "seconds") < this.config.ttl) { 20 | return this.athleticDeckCache; 21 | } 22 | const { data } = await axios_1.default.get(this.config.rankURL, { 23 | timeout: 10000, 24 | responseType: "json", 25 | paramsSerializer: querystring_1.default.stringify, 26 | params: this.config.athleticFetchParams 27 | }); 28 | const athleticDecks = data.slice(0, this.config.rankCount).map(m => m.name); 29 | this.athleticDeckCache = athleticDecks; 30 | this.lastAthleticDeckFetchTime = (0, moment_1.default)(); 31 | return athleticDecks; 32 | } 33 | async getDeckType(deck) { 34 | const deckString = this.deckToString(deck); 35 | const { data } = await axios_1.default.post(this.config.identifierURL, querystring_1.default.stringify({ deck: deckString }), { 36 | timeout: 10000, 37 | responseType: "json" 38 | }); 39 | return data.deck; 40 | } 41 | async checkAthletic(deck) { 42 | try { 43 | const deckType = await this.getDeckType(deck); 44 | if (deckType === '迷之卡组') { 45 | return { success: true, athletic: 0, message: null }; 46 | } 47 | const athleticDecks = await this.getAthleticDecks(); 48 | const athletic = athleticDecks.findIndex(d => d === deckType) + 1; 49 | return { success: true, athletic, message: null }; 50 | } 51 | catch (e) { 52 | return { success: false, message: e.toString() }; 53 | } 54 | } 55 | } 56 | exports.AthleticChecker = AthleticChecker; 57 | -------------------------------------------------------------------------------- /data-manager/entities/DuelLog.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, OneToMany, PrimaryGeneratedColumn} from "typeorm"; 2 | import {DuelLogPlayer} from "./DuelLogPlayer"; 3 | import moment from "moment"; 4 | import _ from "underscore"; 5 | import {CreateAndUpdateTimeBase} from "./CreateAndUpdateTimeBase"; 6 | 7 | @Entity({ 8 | orderBy: { 9 | id: "DESC" 10 | } 11 | }) 12 | export class DuelLog extends CreateAndUpdateTimeBase { 13 | @PrimaryGeneratedColumn({unsigned: true, type: (global as any).PrimaryKeyType as ('bigint' | 'integer') || 'bigint'}) 14 | id: number; 15 | 16 | @Index() 17 | @Column("datetime") 18 | time: Date; 19 | 20 | @Index() 21 | @Column({type: "varchar", length: 20}) 22 | name: string; 23 | 24 | @Column("int") 25 | roomId: number; 26 | 27 | @Column((global as any).PrimaryKeyType as ('bigint' | 'integer') || 'bigint') 28 | cloudReplayId: number; // not very needed to become a relation 29 | 30 | @Column({type: "varchar", length: 256}) 31 | replayFileName: string; 32 | 33 | @Column("tinyint", {unsigned: true}) 34 | roomMode: number; 35 | 36 | @Index() 37 | @Column("tinyint", {unsigned: true}) 38 | duelCount: number; 39 | 40 | @OneToMany(() => DuelLogPlayer, player => player.duelLog) 41 | players: DuelLogPlayer[]; 42 | 43 | getViewString() { 44 | const viewPlayers = _.clone(this.players); 45 | viewPlayers.sort((p1, p2) => p1.pos - p2.pos); 46 | const playerString = viewPlayers[0].realName.split("$")[0] + (viewPlayers[2] ? "+" + viewPlayers[2].realName.split("$")[0] : "") + " VS " + (viewPlayers[1] ? viewPlayers[1].realName.split("$")[0] : "AI") + (viewPlayers[3] ? "+" + viewPlayers[3].realName.split("$")[0] : ""); 47 | return `<${this.id}> ${playerString} ${moment(this.time).format("YYYY-MM-DD HH-mm-ss")}`; 48 | } 49 | 50 | getViewJSON(tournamentModeSettings: any) { 51 | const data = { 52 | id: this.id, 53 | time: moment(this.time).format("YYYY-MM-DD HH:mm:ss"), 54 | name: this.name + (tournamentModeSettings.show_info ? " (Duel:" + this.duelCount + ")" : ""), 55 | roomid: this.roomId, 56 | cloud_replay_id: "R#" + this.cloudReplayId, 57 | replay_filename: this.replayFileName, 58 | roommode: this.roomMode, 59 | players: this.players.map(player => { 60 | return { 61 | pos: player.pos, 62 | is_first: player.isFirst === 1, 63 | name: player.name + (tournamentModeSettings.show_ip ? " (IP: " + player.ip.slice(7) + ")" : "") + (tournamentModeSettings.show_info && !(this.roomMode === 2 && player.pos % 2 > 0) ? " (Score:" + player.score + " LP:" + (player.lp != null ? player.lp : "???") + (this.roomMode !== 2 ? " Cards:" + (player.cardCount != null ? player.cardCount : "???") : "") + ")" : ""), 64 | winner: player.winner === 1 65 | } 66 | }) 67 | } 68 | return data; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SRVPro 2 | 一个YGOPro服务器。 3 | 4 | 现用于[萌卡](https://mycard.moe/),[YGOPro 233服](https://ygo233.com/)和[YGOPro Koishi服](http://koishi.222diy.gdn/)。 5 | 6 | ### 支持功能 7 | * Linux上运行 8 | * Windows上运行 9 | * 玩家输入同一房名约战 10 | * 玩家不指定房间名,自动匹配在线玩家 11 | * 房间列表json 12 | * 广播消息 13 | * 召唤台词 14 | * 先行卡一键更新 15 | * WindBot在线AI 16 | * 萌卡用户登陆 17 | * 竞赛模式锁定玩家卡组 18 | * 竞赛模式后台保存录像 19 | * 竞赛模式自动加时赛系统(规则可调) 20 | * 0 正常加时赛规则 21 | * 1 YGOCore战队联盟第十二届联赛使用规则 22 | * 2 正常加时赛规则 + 1胜规则 23 | * 3 2018年7月适用的OCG/TCG加时赛规则 24 | * 断线重连 25 | 26 | ### 不支持功能 27 | * 在线聊天室 28 | 29 | ### 使用方法 30 | * 可参考[wiki](https://github.com/moecube/srvpro/wiki)安装 31 | * 手动安装: 32 | * `git clone https://github.com/moecube/srvpro.git` 33 | * `cd srvpro` 34 | * `npm install` 35 | * 安装修改后的YGOPro服务端:https://github.com/moecube/ygopro/tree/server 36 | * `node ygopro-server.js`即可运行 37 | * 简易的控制台在 http://srvpro.ygo233.com/dashboard.html 或 http://srvpro-cn.ygo233.com/dashboard.html 38 | * 使用本项目的Docker镜像: https://hub.docker.com/r/mycard/ygopro-server/ 39 | 40 | * 镜像标签 41 | * `mycard/ygopro-server:latest`: 完整镜像 42 | * `mycard/ygopro-server:lite`: 基本镜像,云录像和人机对战功能需要配合`redis`和`nanahira/windbot`这两个镜像使用。 43 | 44 | * 端口 45 | * `7911`: YGOPro端口 46 | * `7922`: 管理后台端口 47 | 48 | * 数据卷 49 | * `/ygopro-server/config`: SRVPro配置文件数据卷 50 | * `/ygopro-server/ygopro/expansions`: YGOPro额外卡片数据卷 51 | * `/ygopro-server/decks`: 竞赛模式卡组数据卷 52 | * `/ygopro-server/replays`: 竞赛模式录像数据卷 53 | 54 | * 若使用竞赛模式启动服务器,建议把启动命令修改为`pm2-docker start /ygopro-server/data/pm2-docker-tournament.js`。 55 | 56 | ### 高级功能 57 | * 待补充说明 58 | * 简易的先行卡更新控制台在 http://srvpro.ygo233.com/pre-dashboard.html 或 http://srvpro-cn.ygo233.com/pre-dashboard.html 59 | 60 | ### 开发计划 61 | * 重做CTOS和STOC部分 62 | * 模块化附加功能 63 | * 房名代码 64 | * 随机对战 65 | * 召唤台词 66 | * WindBot 67 | * 云录像 68 | * 比赛模式 69 | * 先行卡更新 70 | * 用户账号系统和管理员账号系统 71 | * 云录像更换存储方式 72 | 73 | ### TODO 74 | * refactoring CTOS and STOC 75 | * change features to modules 76 | * room name parsing 77 | * random duel 78 | * summon dialogues 79 | * WindBot 80 | * cloud replay 81 | * tournament mode 82 | * expansions updater 83 | * user and admin account system 84 | * new database for cloud replay 85 | 86 | ### License 87 | SRVPro 88 | 89 | Copyright (C) 2013-2018 MoeCube Team 90 | 91 | This program is free software: you can redistribute it and/or modify 92 | it under the terms of the GNU Affero General Public License as 93 | published by the Free Software Foundation, either version 3 of the 94 | License, or (at your option) any later version. 95 | 96 | This program is distributed in the hope that it will be useful, 97 | but WITHOUT ANY WARRANTY; without even the implied warranty of 98 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 99 | GNU Affero General Public License for more details. 100 | 101 | You should have received a copy of the GNU Affero General Public License 102 | along with this program. If not, see . 103 | -------------------------------------------------------------------------------- /data-manager/entities/CloudReplay.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.CloudReplay = void 0; 16 | const typeorm_1 = require("typeorm"); 17 | const CloudReplayPlayer_1 = require("./CloudReplayPlayer"); 18 | const underscore_1 = __importDefault(require("underscore")); 19 | const moment_1 = __importDefault(require("moment")); 20 | const CreateAndUpdateTimeBase_1 = require("./CreateAndUpdateTimeBase"); 21 | let CloudReplay = class CloudReplay extends CreateAndUpdateTimeBase_1.CreateAndUpdateTimeBase { 22 | fromBuffer(buffer) { 23 | this.data = buffer.toString("base64"); 24 | } 25 | toBuffer() { 26 | return Buffer.from(this.data, "base64"); 27 | } 28 | getDateString() { 29 | return (0, moment_1.default)(this.date).format('YYYY-MM-DD HH:mm:ss'); 30 | } 31 | getPlayerNamesString() { 32 | const playerInfos = underscore_1.default.clone(this.players); 33 | playerInfos.sort((p1, p2) => p1.pos - p2.pos); 34 | return playerInfos[0].name + (playerInfos[2] ? "+" + playerInfos[2].name : "") + " VS " + (playerInfos[1] ? playerInfos[1].name : "AI") + (playerInfos[3] ? "+" + playerInfos[3].name : ""); 35 | } 36 | getDisplayString() { 37 | return `R#${this.id} ${this.getPlayerNamesString()} ${this.getDateString()}`; 38 | } 39 | }; 40 | exports.CloudReplay = CloudReplay; 41 | __decorate([ 42 | (0, typeorm_1.PrimaryColumn)({ unsigned: true, type: global.PrimaryKeyType || 'bigint' }), 43 | __metadata("design:type", Number) 44 | ], CloudReplay.prototype, "id", void 0); 45 | __decorate([ 46 | (0, typeorm_1.Column)({ type: "text" }), 47 | __metadata("design:type", String) 48 | ], CloudReplay.prototype, "data", void 0); 49 | __decorate([ 50 | (0, typeorm_1.Index)(), 51 | (0, typeorm_1.Column)({ type: "datetime" }), 52 | __metadata("design:type", Date) 53 | ], CloudReplay.prototype, "date", void 0); 54 | __decorate([ 55 | (0, typeorm_1.OneToMany)(() => CloudReplayPlayer_1.CloudReplayPlayer, player => player.cloudReplay), 56 | __metadata("design:type", Array) 57 | ], CloudReplay.prototype, "players", void 0); 58 | exports.CloudReplay = CloudReplay = __decorate([ 59 | (0, typeorm_1.Entity)({ 60 | orderBy: { 61 | date: "DESC" 62 | } 63 | }) 64 | ], CloudReplay); 65 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - build2 4 | - deploy 5 | 6 | variables: 7 | GIT_DEPTH: "1" 8 | 9 | .docker-op: 10 | before_script: 11 | - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY 12 | 13 | .docker-x86: 14 | extends: .docker-op 15 | tags: 16 | - docker-noavx2 17 | variables: 18 | ARCH: x86 19 | 20 | .docker-arm: 21 | extends: .docker-op 22 | tags: 23 | - docker-arm 24 | variables: 25 | ARCH: arm 26 | 27 | .build_lite: 28 | stage: build 29 | script: 30 | - docker build --pull --no-cache -f ./Dockerfile.lite -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$ARCH-lite . 31 | - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$ARCH-lite 32 | 33 | build_lite_x86: 34 | extends: 35 | - .docker-x86 36 | - .build_lite 37 | 38 | build_lite_arm: 39 | extends: 40 | - .docker-arm 41 | - .build_lite 42 | 43 | .build_full: 44 | stage: build2 45 | script: 46 | - docker build --build-arg BASE_IMAGE=$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$ARCH-lite --pull --no-cache -t 47 | $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$ARCH-full . 48 | - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$ARCH-full 49 | 50 | build_full_x86: 51 | extends: 52 | - .docker-x86 53 | - .build_full 54 | 55 | build_full_arm: 56 | extends: 57 | - .docker-arm 58 | - .build_full 59 | 60 | .deploy_image: 61 | stage: deploy 62 | extends: .docker-x86 63 | script: 64 | - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86-$DEPLOY_TYPE 65 | - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm-$DEPLOY_TYPE 66 | - docker manifest create $CI_REGISTRY_IMAGE:$DEPLOY_TAG $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86-$DEPLOY_TYPE $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm-$DEPLOY_TYPE 67 | - docker manifest push $CI_REGISTRY_IMAGE:$DEPLOY_TAG 68 | 69 | deploy_lite: 70 | extends: .deploy_image 71 | variables: 72 | DEPLOY_TYPE: lite 73 | DEPLOY_TAG: $CI_COMMIT_REF_SLUG-lite 74 | 75 | deploy_full: 76 | extends: .deploy_image 77 | variables: 78 | DEPLOY_TYPE: full 79 | DEPLOY_TAG: $CI_COMMIT_REF_SLUG-full 80 | 81 | deploy_branch: 82 | extends: .deploy_image 83 | variables: 84 | DEPLOY_TYPE: full 85 | DEPLOY_TAG: $CI_COMMIT_REF_SLUG 86 | 87 | deploy_latest_lite: 88 | extends: .deploy_image 89 | variables: 90 | DEPLOY_TYPE: lite 91 | DEPLOY_TAG: lite 92 | only: 93 | - master 94 | 95 | deploy_latest_full: 96 | extends: .deploy_image 97 | variables: 98 | DEPLOY_TYPE: full 99 | DEPLOY_TAG: full 100 | only: 101 | - master 102 | 103 | deploy_latest: 104 | extends: .deploy_image 105 | variables: 106 | DEPLOY_TYPE: full 107 | DEPLOY_TAG: latest 108 | only: 109 | - master 110 | 111 | upload_stuff_to_minio: 112 | stage: deploy 113 | tags: 114 | - linux 115 | image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86-full 116 | script: 117 | - apt update ; apt -y install python3-pip 118 | - pip3 install -U -i https://mirrors.aliyun.com/pypi/simple/ awscli 119 | - cd /ygopro-server 120 | - mkdir /dist 121 | - tar zcfv /dist/ygopro-server.tar.gz ./* 122 | - aws s3 --endpoint=https://minio.mycard.moe:9000 cp 123 | /dist/ygopro-server.tar.gz s3://mycard/srvpro/ygopro-server.tar.gz 124 | only: 125 | - master 126 | -------------------------------------------------------------------------------- /data-manager/entities/RandomDuelScore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.RandomDuelScore = void 0; 13 | const typeorm_1 = require("typeorm"); 14 | const CreateAndUpdateTimeBase_1 = require("./CreateAndUpdateTimeBase"); 15 | let RandomDuelScore = class RandomDuelScore extends CreateAndUpdateTimeBase_1.CreateAndUpdateTimeBase { 16 | getDisplayName() { 17 | return this.name.split("$")[0]; 18 | } 19 | win() { 20 | ++this.winCount; 21 | ++this.winCombo; 22 | } 23 | lose() { 24 | ++this.loseCount; 25 | this.winCombo = 0; 26 | } 27 | flee() { 28 | ++this.fleeCount; 29 | this.lose(); 30 | } 31 | getScoreText(displayName) { 32 | const total = this.winCount + this.loseCount; 33 | if (this.winCount < 2 && total < 3) { 34 | return `${displayName} \${random_score_not_enough}`; 35 | } 36 | if (this.winCombo >= 2) { 37 | return `\${random_score_part1}${displayName} \${random_score_part2} ${Math.ceil(this.winCount / total * 100)}\${random_score_part3} ${Math.ceil(this.fleeCount / total * 100)}\${random_score_part4_combo}${this.winCombo}\${random_score_part5_combo}`; 38 | } 39 | else { 40 | //return displayName + " 的今日战绩:胜率" + Math.ceil(this.winCount/total*100) + "%,逃跑率" + Math.ceil(this.fleeCount/total*100) + "%," + this.winCombo + "连胜中!" 41 | return `\${random_score_part1}${displayName} \${random_score_part2} ${Math.ceil(this.winCount / total * 100)}\${random_score_part3} ${Math.ceil(this.fleeCount / total * 100)}\${random_score_part4}`; 42 | } 43 | } 44 | }; 45 | exports.RandomDuelScore = RandomDuelScore; 46 | __decorate([ 47 | (0, typeorm_1.PrimaryColumn)({ type: "varchar", length: 20 }), 48 | __metadata("design:type", String) 49 | ], RandomDuelScore.prototype, "name", void 0); 50 | __decorate([ 51 | (0, typeorm_1.Index)(), 52 | (0, typeorm_1.Column)("int", { unsigned: true, default: 0 }), 53 | __metadata("design:type", Number) 54 | ], RandomDuelScore.prototype, "winCount", void 0); 55 | __decorate([ 56 | (0, typeorm_1.Index)(), 57 | (0, typeorm_1.Column)("int", { unsigned: true, default: 0 }), 58 | __metadata("design:type", Number) 59 | ], RandomDuelScore.prototype, "loseCount", void 0); 60 | __decorate([ 61 | (0, typeorm_1.Index)(), 62 | (0, typeorm_1.Column)("int", { unsigned: true, default: 0 }), 63 | __metadata("design:type", Number) 64 | ], RandomDuelScore.prototype, "fleeCount", void 0); 65 | __decorate([ 66 | (0, typeorm_1.Column)("int", { unsigned: true, default: 0 }), 67 | __metadata("design:type", Number) 68 | ], RandomDuelScore.prototype, "winCombo", void 0); 69 | exports.RandomDuelScore = RandomDuelScore = __decorate([ 70 | (0, typeorm_1.Entity)() 71 | ], RandomDuelScore); 72 | -------------------------------------------------------------------------------- /roomlist.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | (function() { 3 | var WebSocketServer, _delete, broadcast, clients, create, init, room_data, server, settings, start, update, url; 4 | 5 | WebSocketServer = require('ws').Server; 6 | 7 | url = require('url'); 8 | 9 | settings = global.settings; 10 | 11 | server = null; 12 | 13 | room_data = function(room) { 14 | var client; 15 | return { 16 | id: room.name, 17 | title: room.title || room.name, 18 | user: { 19 | username: room.username 20 | }, 21 | users: (function() { 22 | var i, len, ref, results; 23 | ref = room.players; 24 | results = []; 25 | for (i = 0, len = ref.length; i < len; i++) { 26 | client = ref[i]; 27 | results.push({ 28 | username: client.name, 29 | position: client.pos 30 | }); 31 | } 32 | return results; 33 | })(), 34 | options: room.get_roomlist_hostinfo(), // Should be updated when MyCard client updates 35 | arena: settings.modules.arena_mode.enabled && room.arena && settings.modules.arena_mode.mode 36 | }; 37 | }; 38 | 39 | clients = new Set(); 40 | 41 | init = function(http_server, ROOM_all) { 42 | server = new WebSocketServer({ 43 | server: http_server 44 | }); 45 | return server.on('connection', function(connection, upgradeReq) { 46 | var room; 47 | connection.filter = url.parse(upgradeReq.url, true).query.filter || 'waiting'; 48 | connection.send(JSON.stringify({ 49 | event: 'init', 50 | data: (function() { 51 | var i, len, results; 52 | results = []; 53 | for (i = 0, len = ROOM_all.length; i < len; i++) { 54 | room = ROOM_all[i]; 55 | if (room && room.established && (connection.filter === 'started' || !room.private) && ((room.duel_stage !== 0) === (connection.filter === 'started'))) { 56 | results.push(room_data(room)); 57 | } 58 | } 59 | return results; 60 | })() 61 | })); 62 | clients.add(connection); 63 | return connection.on('close', function() { 64 | if (clients.has(connection)) { 65 | return clients.delete(connection); 66 | } 67 | }); 68 | }); 69 | }; 70 | 71 | create = function(room) { 72 | if (!room.private) { 73 | return broadcast('create', room_data(room), 'waiting'); 74 | } 75 | }; 76 | 77 | update = function(room) { 78 | if (!room.private) { 79 | return broadcast('update', room_data(room), 'waiting'); 80 | } 81 | }; 82 | 83 | start = function(room) { 84 | if (!room.private) { 85 | broadcast('delete', room.name, 'waiting'); 86 | } 87 | return broadcast('create', room_data(room), 'started'); 88 | }; 89 | 90 | _delete = function(room) { 91 | if (room.duel_stage !== 0) { 92 | return broadcast('delete', room.name, 'started'); 93 | } else { 94 | if (!room.private) { 95 | return broadcast('delete', room.name, 'waiting'); 96 | } 97 | } 98 | }; 99 | 100 | broadcast = function(event, data, filter) { 101 | var connection, i, len, message, ref, results; 102 | if (!server) { 103 | return; 104 | } 105 | message = JSON.stringify({ 106 | event: event, 107 | data: data 108 | }); 109 | ref = Array.from(clients.values()); 110 | results = []; 111 | for (i = 0, len = ref.length; i < len; i++) { 112 | connection = ref[i]; 113 | if (connection.filter === filter) { 114 | try { 115 | results.push(connection.send(message)); 116 | } catch (error) {} 117 | } 118 | } 119 | return results; 120 | }; 121 | 122 | module.exports = { 123 | init: init, 124 | create: create, 125 | update: update, 126 | start: start, 127 | delete: _delete 128 | }; 129 | 130 | }).call(this); 131 | -------------------------------------------------------------------------------- /ygopro.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | _.str = require 'underscore.string' 3 | _.mixin(_.str.exports()) 4 | 5 | Struct = require('./struct.js').Struct 6 | loadJSON = require('load-json-file').sync 7 | 8 | @i18ns = loadJSON './data/i18n.json' 9 | 10 | @i18nR = {} 11 | @reloadI18nR = () -> 12 | for lang, data of @i18ns 13 | @i18nR[lang]={} 14 | for key, text of data 15 | @i18nR[lang][key]={ 16 | regex: new RegExp("\\$\\{"+key+"\\}",'g'), 17 | text: text 18 | } 19 | 20 | @reloadI18nR() 21 | 22 | YGOProMessagesHelper = require("./YGOProMessages.js").YGOProMessagesHelper # 为 SRVPro2 准备的库,这里拿这个库只用来测试,SRVPro1 对异步支持不是特别完善,因此不会有很多异步优化 23 | @helper = new YGOProMessagesHelper(9000) 24 | 25 | @structs = @helper.structs 26 | @structs_declaration = @helper.structs_declaration 27 | @typedefs = @helper.typedefs 28 | @proto_structs = @helper.proto_structs 29 | @constants = @helper.constants 30 | 31 | translateHandler = (handler) -> 32 | return (buffer, info, datas, params)-> 33 | await return await handler(buffer, info, params.client, params.server, datas) 34 | 35 | @stoc_follow = (proto, synchronous, callback)-> 36 | @helper.addHandler("STOC_#{proto}", translateHandler(callback), synchronous, 1) 37 | return 38 | @stoc_follow_before = (proto, synchronous, callback)-> 39 | @helper.addHandler("STOC_#{proto}", translateHandler(callback), synchronous, 0) 40 | return 41 | @stoc_follow_after = (proto, synchronous, callback)-> 42 | @helper.addHandler("STOC_#{proto}", translateHandler(callback), synchronous, 2) 43 | return 44 | @ctos_follow = (proto, synchronous, callback)-> 45 | @helper.addHandler("CTOS_#{proto}", translateHandler(callback), synchronous, 1) 46 | return 47 | @ctos_follow_before = (proto, synchronous, callback)-> 48 | @helper.addHandler("CTOS_#{proto}", translateHandler(callback), synchronous, 0) 49 | return 50 | @ctos_follow_after = (proto, synchronous, callback)-> 51 | @helper.addHandler("CTOS_#{proto}", translateHandler(callback), synchronous, 2) 52 | return 53 | 54 | #消息发送函数,至少要把俩合起来.... 55 | @stoc_send = (socket, proto, info)-> 56 | return @helper.sendMessage(socket, "STOC_#{proto}", info) 57 | 58 | @ctos_send = (socket, proto, info)-> 59 | return @helper.sendMessage(socket, "CTOS_#{proto}", info) 60 | 61 | #util 62 | @split_chat_lines = (msg, player, lang) -> 63 | lines = [] 64 | for line in _.lines(msg) 65 | if player>=10 66 | line="[Server]: "+line 67 | for o,r of @i18nR[lang] 68 | line=line.replace(r.regex, r.text) 69 | lines.push(line) 70 | return lines 71 | 72 | @stoc_send_chat = (client, msg, player = 8)-> 73 | if !client 74 | console.log "err stoc_send_chat" 75 | return 76 | for line in @split_chat_lines(msg, player, client.lang) 77 | await @stoc_send client, 'CHAT', { 78 | player: player 79 | msg: line 80 | } 81 | return 82 | 83 | @stoc_send_chat_to_room = (room, msg, player = 8)-> 84 | if !room 85 | console.log "err stoc_send_chat_to_room" 86 | return 87 | for client in room.players 88 | @stoc_send_chat(client, msg, player) if client 89 | for client in room.watchers 90 | @stoc_send_chat(client, msg, player) if client 91 | room.recordChatMessage(msg, player) 92 | return 93 | 94 | @stoc_send_hint_card_to_room = (room, card)-> 95 | if !room 96 | console.log "err stoc_send_hint_card_to_room" 97 | return 98 | for client in room.players 99 | @stoc_send client, 'GAME_MSG', { 100 | curmsg: 2, 101 | type: 10, 102 | player: 0, 103 | data: card 104 | } if client 105 | for client in room.watchers 106 | @stoc_send client, 'GAME_MSG', { 107 | curmsg: 2, 108 | type: 10, 109 | player: 0, 110 | data: card 111 | } if client 112 | return 113 | 114 | @stoc_die = (client, msg)-> 115 | await @stoc_send_chat(client, msg, @constants.COLORS.RED) 116 | await @stoc_send client, 'ERROR_MSG', { 117 | msg: 1 118 | code: 9 119 | } if client 120 | if client 121 | client.system_kicked = true 122 | client.destroy() 123 | return '_cancel' 124 | -------------------------------------------------------------------------------- /challonge.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.Challonge = void 0; 7 | const axios_1 = __importDefault(require("axios")); 8 | const bunyan_1 = require("bunyan"); 9 | const moment_1 = __importDefault(require("moment")); 10 | const p_queue_1 = __importDefault(require("p-queue")); 11 | class Challonge { 12 | constructor(config) { 13 | this.config = config; 14 | this.queue = new p_queue_1.default({ concurrency: 1 }); 15 | this.log = (0, bunyan_1.createLogger)({ name: 'challonge' }); 16 | } 17 | async getTournamentProcess(noCache = false) { 18 | if (!noCache && this.previous && this.previousTime.isAfter((0, moment_1.default)().subtract(this.config.cache_ttl, 'ms'))) { 19 | return this.previous; 20 | } 21 | try { 22 | const { data: { tournament } } = await axios_1.default.get(`${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}.json`, { 23 | params: { 24 | api_key: this.config.api_key, 25 | include_participants: 1, 26 | include_matches: 1, 27 | }, 28 | timeout: 5000, 29 | }); 30 | this.previous = tournament; 31 | this.previousTime = (0, moment_1.default)(); 32 | return tournament; 33 | } 34 | catch (e) { 35 | this.log.error(`Failed to get tournament ${this.config.tournament_id}: ${e}`); 36 | return; 37 | } 38 | } 39 | async getTournament(noCache = false) { 40 | if (noCache) { 41 | return this.getTournamentProcess(noCache); 42 | } 43 | return this.queue.add(() => this.getTournamentProcess()); 44 | } 45 | async putScore(matchId, match, retried = 0) { 46 | try { 47 | await axios_1.default.put(`${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}/matches/${matchId}.json`, { 48 | api_key: this.config.api_key, 49 | match: match, 50 | }); 51 | this.previous = undefined; 52 | this.previousTime = undefined; 53 | return true; 54 | } 55 | catch (e) { 56 | this.log.error(`Failed to put score for match ${matchId}: ${e}`); 57 | if (retried < 5) { 58 | this.log.info(`Retrying match ${matchId}`); 59 | return this.putScore(matchId, match, retried + 1); 60 | } 61 | else { 62 | this.log.error(`Failed to put score for match ${matchId} after 5 retries`); 63 | return false; 64 | } 65 | } 66 | } 67 | // DELETE /v1/tournaments/${tournament_id}/participants/clear.json?api_key=xxx returns ANY 68 | async clearParticipants() { 69 | try { 70 | await axios_1.default.delete(`${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}/participants/clear.json`, { 71 | params: { 72 | api_key: this.config.api_key 73 | }, 74 | validateStatus: () => true, 75 | }); 76 | return true; 77 | } 78 | catch (e) { 79 | this.log.error(`Failed to clear participants for tournament ${this.config.tournament_id}: ${e}`); 80 | return false; 81 | } 82 | } 83 | // POST /v1/tournaments/${tournament_id}/participants/bulk_add.json { api_key: string, participants: { name: string }[] } returns ANY 84 | async uploadParticipants(participantNames) { 85 | try { 86 | await axios_1.default.post(`${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}/participants/bulk_add.json`, { 87 | api_key: this.config.api_key, 88 | participants: participantNames.map(name => ({ name })), 89 | }); 90 | return true; 91 | } 92 | catch (e) { 93 | this.log.error(`Failed to upload participants for tournament ${this.config.tournament_id}: ${e}`); 94 | return false; 95 | } 96 | } 97 | } 98 | exports.Challonge = Challonge; 99 | -------------------------------------------------------------------------------- /msg-polyfill/polyfillers/0x1361.ts: -------------------------------------------------------------------------------- 1 | import { BasePolyfiller } from "../base-polyfiller"; 2 | 3 | export const gcd = (nums: number[]) => { 4 | const gcdTwo = (a: number, b: number) => { 5 | if (b === 0) return a; 6 | return gcdTwo(b, a % b); 7 | }; 8 | 9 | return nums.reduce((acc, num) => gcdTwo(acc, num)); 10 | } 11 | 12 | export class Polyfiller1361 extends BasePolyfiller { 13 | async polyfillGameMsg(msgTitle: string, buffer: Buffer) { 14 | if (msgTitle === 'CONFIRM_CARDS') { 15 | // buf[0]: MSG_CONFIRM_CARDS 16 | // buf[1]: playerid 17 | // buf[2]: ADDED skip_panel 18 | return this.splice(buffer, 2, 1); 19 | } else if (msgTitle === 'SELECT_CHAIN') { 20 | // buf[0]: MSG_SELECT_CHAIN 21 | // buf[1]: playerid 22 | // buf[2]: size 23 | // buf[3]: spe_count 24 | // buf[REMOVED]: forced 25 | // buf[4-7]: hint_timing player 26 | // buf[8-11]: hint_timing 1-player 27 | // then it's 14 bytes for each item 28 | // item[0]: not related 29 | // item[1]: ADDED forced 30 | // item[2-13] not related 31 | 32 | const size = buffer[2]; 33 | const itemStartOffset = 12; // after the header (up to hint timings) 34 | 35 | // 判断是否存在任何 item 的 forced = 1(在原始 buffer 中判断) 36 | let anyForced = false; 37 | for (let i = 0; i < size; i++) { 38 | const itemOffset = itemStartOffset + i * 14; 39 | const forced = buffer[itemOffset + 1]; 40 | if (forced === 1) { 41 | anyForced = true; 42 | break; 43 | } 44 | } 45 | 46 | // 从后往前 splice 每个 item 的 forced 字段 47 | for (let i = size - 1; i >= 0; i--) { 48 | const itemOffset = itemStartOffset + i * 14; 49 | buffer = this.splice(buffer, itemOffset + 1, 1); // 删除每个 item 的 forced(第 1 字节) 50 | } 51 | 52 | // 最后再插入旧版所需的 forced 标志 53 | return this.insert(buffer, 4, Buffer.from([anyForced ? 1 : 0])); 54 | } else if (msgTitle === 'SELECT_SUM') { 55 | // buf[0]: MSG_SELECT_SUM 56 | // buf[1]: 0 => equal, 1 => greater 57 | // buf[2]: playerid 58 | // buf[3-6]: target_value 59 | // buf[7]: min 60 | // buf[8]: max 61 | // buf[9]: forced_count 62 | // then each item 11 bytes 63 | 64 | // item[0-3] code 65 | // item[4] controler 66 | // item[5] location 67 | // item[6] sequence 68 | // item[7-10] value 69 | 70 | // item[10 + forced_count * 11] card_count 71 | 72 | // same as above items 73 | 74 | const targetValue = buffer.readUInt32LE(3); 75 | const forcedCount = buffer[9]; 76 | const cardCount = buffer[10 + forcedCount * 11]; 77 | const valueOffsets: number[] = []; 78 | for(let i = 0; i < forcedCount; i++) { 79 | const itemOffset = 10 + i * 11; 80 | valueOffsets.push(itemOffset + 7); 81 | } 82 | for(let i = 0; i < cardCount; i++) { 83 | const itemOffset = 11 + forcedCount * 11 + i * 11; 84 | valueOffsets.push(itemOffset + 7); 85 | } 86 | const values = valueOffsets.map(offset => ({ 87 | offset, 88 | value: buffer.readUInt32LE(offset), 89 | })); 90 | if (!values.some(v => v.value & 0x80000000)) { 91 | return; 92 | } 93 | const gcds = [targetValue]; 94 | for(const { value } of values) { 95 | if (value & 0x80000000) { 96 | gcds.push(value & 0x7FFFFFFF); 97 | } else { 98 | const op1 = value & 0xffff; 99 | const op2 = (value >>> 16) & 0xffff; 100 | [op1, op2].filter(v => v > 0) 101 | .forEach(v => gcds.push(v)); 102 | } 103 | } 104 | const gcdValue = gcd(gcds); 105 | buffer.writeUInt32LE(targetValue / gcdValue, 3); 106 | for (const trans of values) { 107 | let target = 0; 108 | const { value } = trans; 109 | if (value & 0x80000000) { 110 | target = ((value & 0x7FFFFFFF) / gcdValue) & 0xffff; 111 | } else { 112 | // shrink op1 and op2 113 | const op1 = value & 0xffff; 114 | const op2 = (value >>> 16) & 0xffff; 115 | target = ((op1 / gcdValue) & 0xffff) | ((((op2 / gcdValue) & 0xffff) << 16) >>> 0); 116 | } 117 | buffer.writeUInt32LE(target, trans.offset); 118 | } 119 | } 120 | return; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /data-manager/entities/DuelLogPlayer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var DuelLogPlayer_1; 12 | Object.defineProperty(exports, "__esModule", { value: true }); 13 | exports.DuelLogPlayer = void 0; 14 | const typeorm_1 = require("typeorm"); 15 | const BasePlayer_1 = require("./BasePlayer"); 16 | const DuelLog_1 = require("./DuelLog"); 17 | const DeckEncoder_1 = require("../DeckEncoder"); 18 | let DuelLogPlayer = DuelLogPlayer_1 = class DuelLogPlayer extends BasePlayer_1.BasePlayer { 19 | setStartDeck(deck) { 20 | if (!deck) { 21 | this.startDeckBuffer = null; 22 | return; 23 | } 24 | this.startDeckBuffer = (0, DeckEncoder_1.encodeDeck)(deck).toString("base64"); 25 | } 26 | getStartDeck() { 27 | return (0, DeckEncoder_1.decodeDeck)(Buffer.from(this.startDeckBuffer, "base64")); 28 | } 29 | setCurrentDeck(deck) { 30 | if (!deck) { 31 | this.currentDeckBuffer = null; 32 | return; 33 | } 34 | this.currentDeckBuffer = (0, DeckEncoder_1.encodeDeck)(deck).toString("base64"); 35 | } 36 | getCurrentDeck() { 37 | return (0, DeckEncoder_1.decodeDeck)(Buffer.from(this.currentDeckBuffer, "base64")); 38 | } 39 | static fromDuelLogPlayerInfo(info) { 40 | const p = new DuelLogPlayer_1(); 41 | p.name = info.name; 42 | p.pos = info.pos; 43 | p.realName = info.realName; 44 | p.lp = info.lp; 45 | p.ip = info.ip; 46 | p.score = info.score; 47 | p.cardCount = info.cardCount; 48 | p.isFirst = info.isFirst ? 1 : 0; 49 | p.winner = info.winner ? 1 : 0; 50 | p.startDeckBuffer = info.startDeckBuffer?.toString("base64") || null; 51 | p.setCurrentDeck(info.deck); 52 | return p; 53 | } 54 | }; 55 | exports.DuelLogPlayer = DuelLogPlayer; 56 | __decorate([ 57 | (0, typeorm_1.Index)(), 58 | (0, typeorm_1.Column)({ type: "varchar", length: 20 }), 59 | __metadata("design:type", String) 60 | ], DuelLogPlayer.prototype, "realName", void 0); 61 | __decorate([ 62 | (0, typeorm_1.Column)({ type: "varchar", length: 64, nullable: true }), 63 | __metadata("design:type", String) 64 | ], DuelLogPlayer.prototype, "ip", void 0); 65 | __decorate([ 66 | (0, typeorm_1.Column)("tinyint", { unsigned: true }), 67 | __metadata("design:type", Number) 68 | ], DuelLogPlayer.prototype, "isFirst", void 0); 69 | __decorate([ 70 | (0, typeorm_1.Index)(), 71 | (0, typeorm_1.Column)("tinyint"), 72 | __metadata("design:type", Number) 73 | ], DuelLogPlayer.prototype, "score", void 0); 74 | __decorate([ 75 | (0, typeorm_1.Column)("int", { nullable: true }), 76 | __metadata("design:type", Number) 77 | ], DuelLogPlayer.prototype, "lp", void 0); 78 | __decorate([ 79 | (0, typeorm_1.Column)("smallint", { nullable: true }), 80 | __metadata("design:type", Number) 81 | ], DuelLogPlayer.prototype, "cardCount", void 0); 82 | __decorate([ 83 | (0, typeorm_1.Column)("text", { nullable: true }), 84 | __metadata("design:type", String) 85 | ], DuelLogPlayer.prototype, "startDeckBuffer", void 0); 86 | __decorate([ 87 | (0, typeorm_1.Column)("text", { nullable: true }), 88 | __metadata("design:type", String) 89 | ], DuelLogPlayer.prototype, "currentDeckBuffer", void 0); 90 | __decorate([ 91 | (0, typeorm_1.Column)("tinyint"), 92 | __metadata("design:type", Number) 93 | ], DuelLogPlayer.prototype, "winner", void 0); 94 | __decorate([ 95 | (0, typeorm_1.ManyToOne)(() => DuelLog_1.DuelLog, duelLog => duelLog.players), 96 | __metadata("design:type", DuelLog_1.DuelLog) 97 | ], DuelLogPlayer.prototype, "duelLog", void 0); 98 | exports.DuelLogPlayer = DuelLogPlayer = DuelLogPlayer_1 = __decorate([ 99 | (0, typeorm_1.Entity)() 100 | ], DuelLogPlayer); 101 | -------------------------------------------------------------------------------- /ygopro-auth.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Main script of new dashboard account system. 3 | The account list file is stored at `./config/admin_user.json`. The users are stored at `users`. 4 | The key is the username. The `permissions` field could be a string, using a permission set from the example, or an object, to define a specific set of permissions. 5 | eg. An account for a judge could be as follows, to use the default permission of judges, 6 | "username": { 7 | "password": "123456", 8 | "enabled": true, 9 | "permissions": "judge" 10 | }, 11 | or as follows, to use a specific set of permissions. 12 | "username": { 13 | "password": "123456", 14 | "enabled": true, 15 | "permissions": { 16 | "get_rooms": true, 17 | "duel_log": true, 18 | "download_replay": true, 19 | "deck_dashboard_read": true, 20 | "deck_dashboard_write": true, 21 | "shout": true, 22 | "kick_user": true, 23 | "start_death": true 24 | } 25 | }, 26 | ### 27 | fs = require 'fs' 28 | loadJSON = require('load-json-file').sync 29 | loadJSONPromise = require('load-json-file') 30 | moment = require 'moment' 31 | moment.updateLocale('zh-cn', { 32 | relativeTime: { 33 | future: '%s内', 34 | past: '%s前', 35 | s: '%d秒', 36 | m: '1分钟', 37 | mm: '%d分钟', 38 | h: '1小时', 39 | hh: '%d小时', 40 | d: '1天', 41 | dd: '%d天', 42 | M: '1个月', 43 | MM: '%d个月', 44 | y: '1年', 45 | yy: '%d年' 46 | } 47 | }) 48 | 49 | bunyan = require 'bunyan' 50 | log = bunyan.createLogger name: "auth" 51 | util = require 'util' 52 | 53 | if not fs.existsSync('./logs') 54 | fs.mkdirSync('./logs') 55 | 56 | add_log = (message) -> 57 | mt = moment() 58 | log.info(message) 59 | text = mt.format('YYYY-MM-DD HH:mm:ss') + " --> " + message + "\n" 60 | res = false 61 | try 62 | await fs.promises.appendFile("./logs/"+mt.format('YYYY-MM-DD')+".log", text) 63 | res = true 64 | catch 65 | res = false 66 | return res 67 | 68 | 69 | default_data = loadJSON('./data/default_data.json') 70 | setting_save = (settings) -> 71 | try 72 | await fs.promises.writeFile(settings.file, JSON.stringify(settings, null, 2)) 73 | catch e 74 | add_log("save fail"); 75 | return 76 | 77 | default_data = loadJSON('./data/default_data.json') 78 | 79 | try 80 | users = loadJSON('./config/admin_user.json') 81 | catch 82 | users = default_data.users 83 | setting_save(users) 84 | 85 | save = () -> 86 | return await setting_save(users) 87 | 88 | reload = () -> 89 | user_backup = users 90 | try 91 | users = await loadJSONPromise('./config/admin_user.json') 92 | catch 93 | users = user_backup 94 | await add_log("Invalid user data JSON") 95 | return 96 | 97 | check_permission = (user, permission_required) -> 98 | _permission = user.permissions 99 | permission = _permission 100 | if typeof(permission) != 'object' 101 | permission = users.permission_examples[_permission] 102 | if !permission 103 | await add_log("Permision not set:"+_permission) 104 | return false 105 | return permission[permission_required] 106 | 107 | @auth = (name, pass, permission_required, action = 'unknown', no_log) -> 108 | await reload() 109 | user = users.users[name] 110 | if !user 111 | await add_log("Unknown user login. User: "+ name+", Permission needed: "+ permission_required+", Action: " +action) 112 | return false 113 | if user.password != pass 114 | await add_log("Unauthorized user login. User: "+ name+", Permission needed: "+ permission_required+", Action: " +action) 115 | return false 116 | if !user.enabled 117 | await add_log("Disabled user login. User: "+ name+", Permission needed: "+ permission_required+", Action: " +action) 118 | return false 119 | if !await check_permission(user, permission_required) 120 | await add_log("Permission denied. User: "+ name+", Permission needed: "+ permission_required+", Action: " +action) 121 | return false 122 | if !no_log 123 | await add_log("Operation success. User: "+ name+", Permission needed: "+ permission_required+", Action: " +action) 124 | return true 125 | 126 | @add_user = (name, pass, enabled, permissions) -> 127 | await reload() 128 | if users.users[name] 129 | return false 130 | users.users[name] = { 131 | "password": pass, 132 | "enabled": enabled, 133 | "permissions": permissions 134 | } 135 | await save() 136 | return true 137 | 138 | @delete_user = (name) -> 139 | await reload() 140 | if !users.users[name] 141 | return false 142 | delete users.users[name] 143 | await save() 144 | return 145 | 146 | @update_user = (name, key, value) -> 147 | await reload() 148 | if !users.users[name] 149 | return false 150 | users.users[name][key] = value 151 | await save() 152 | return 153 | -------------------------------------------------------------------------------- /data/structs.json: -------------------------------------------------------------------------------- 1 | { 2 | "HostInfo": [ 3 | {"name": "lflist", "type": "unsigned int"}, 4 | {"name": "rule", "type": "unsigned char"}, 5 | {"name": "mode", "type": "unsigned char"}, 6 | {"name": "duel_rule", "type": "unsigned char"}, 7 | {"name": "no_check_deck", "type": "bool"}, 8 | {"name": "no_shuffle_deck", "type": "bool"}, 9 | {"name": "start_lp", "type": "unsigned int"}, 10 | {"name": "start_hand", "type": "unsigned char"}, 11 | {"name": "draw_count", "type": "unsigned char"}, 12 | {"name": "time_limit", "type": "unsigned short"} 13 | ], 14 | "HostPacket": [ 15 | {"name": "identifier", "type": "unsigned short"}, 16 | {"name": "version", "type": "unsigned short"}, 17 | {"name": "port", "type": "unsigned short"}, 18 | {"name": "ipaddr", "type": "unsigned int"}, 19 | {"name": "name", "type": "unsigned short", "length": 20, "encoding": "UTF-16LE"}, 20 | {"name": "host", "type": "HostInfo"} 21 | ], 22 | "HostRequest": [ 23 | {"name": "identifier", "type": "unsigned short"} 24 | ], 25 | "CTOS_HandResult": [ 26 | {"name": "res", "type": "unsigned char"} 27 | ], 28 | "CTOS_TPResult": [ 29 | {"name": "res", "type": "unsigned char"} 30 | ], 31 | "CTOS_PlayerInfo": [ 32 | {"name": "name", "type": "unsigned short", "length": 20, "encoding": "UTF-16LE"} 33 | ], 34 | "CTOS_CreateGame": [ 35 | {"name": "info", "type": "HostInfo"}, 36 | {"name": "name", "type": "unsigned short", "length": 20, "encoding": "UTF-16LE"}, 37 | {"name": "pass", "type": "unsigned short", "length": 20, "encoding": "UTF-16LE"} 38 | ], 39 | "CTOS_JoinGame": [ 40 | {"name": "version", "type": "unsigned short"}, 41 | {"name": "align", "type": "unsigned short"}, 42 | {"name": "gameid", "type": "unsigned int"}, 43 | {"name": "pass", "type": "unsigned short", "length": 20, "encoding": "UTF-16LE"} 44 | ], 45 | "CTOS_ExternalAddress": [ 46 | {"name": "real_ip", "type": "unsigned int"}, 47 | {"name": "hostname", "type": "unsigned short", "length":"256", "encoding": "UTF-16LE"} 48 | ], 49 | "CTOS_Kick": [ 50 | {"name": "pos", "type": "unsigned char"} 51 | ], 52 | "STOC_ErrorMsg": [ 53 | {"name": "msg", "type": "unsigned char"}, 54 | {"name": "align1", "type": "unsigned char"}, 55 | {"name": "align2", "type": "unsigned char"}, 56 | {"name": "align3", "type": "unsigned char"}, 57 | {"name": "code", "type": "unsigned int"} 58 | ], 59 | "STOC_HandResult": [ 60 | {"name": "res1", "type": "unsigned char"}, 61 | {"name": "res2", "type": "unsigned char"} 62 | ], 63 | "STOC_CreateGame": [ 64 | {"name": "gameid", "type": "unsigned int"} 65 | ], 66 | "STOC_JoinGame": [ 67 | {"name": "info", "type": "HostInfo"} 68 | ], 69 | "STOC_TypeChange": [ 70 | {"name": "type", "type": "unsigned char"} 71 | ], 72 | "STOC_ExitGame": [ 73 | {"name": "pos", "type": "unsigned char"} 74 | ], 75 | "STOC_TimeLimit": [ 76 | {"name": "player", "type": "unsigned char"}, 77 | {"name": "left_time", "type": "unsigned short"} 78 | ], 79 | "STOC_Chat": [ 80 | {"name": "player", "type": "unsigned short"}, 81 | {"name": "msg", "type": "unsigned short", "length": 255, "encoding": "UTF-16LE"} 82 | ], 83 | "STOC_HS_PlayerEnter": [ 84 | {"name": "name", "type": "unsigned short", "length": 20, "encoding": "UTF-16LE"}, 85 | {"name": "pos", "type": "unsigned char"}, 86 | {"name": "padding", "type": "unsigned char"} 87 | ], 88 | "STOC_HS_PlayerChange": [ 89 | {"name": "status", "type": "unsigned char"} 90 | ], 91 | "STOC_HS_WatchChange": [ 92 | {"name": "watch_count", "type": "unsigned short"} 93 | ], 94 | "GameMsg_Hint_Card_only": [ 95 | {"name": "curmsg", "type": "word8Ule"}, 96 | {"name": "type", "type": "word8"}, 97 | {"name": "player", "type": "word8"}, 98 | {"name": "data", "type": "word32Ule"} 99 | ], 100 | "deck": [ 101 | {"name": "mainc", "type": "unsigned int"}, 102 | {"name": "sidec", "type": "unsigned int"}, 103 | {"name": "deckbuf", "type": "unsigned int", "length": 90} 104 | ], 105 | "chat": [ 106 | {"name": "msg", "type": "unsigned short", "length":"255", "encoding": "UTF-16LE"} 107 | ], 108 | "STOC_DeckCount": [ 109 | {"name": "mainc_s", "type": "unsigned short"}, 110 | {"name": "sidec_s", "type": "unsigned short"}, 111 | {"name": "extrac_s", "type": "unsigned short"}, 112 | {"name": "mainc_o", "type": "unsigned short"}, 113 | {"name": "sidec_o", "type": "unsigned short"}, 114 | {"name": "extrac_o", "type": "unsigned short"} 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /data-manager/entities/DuelLog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.DuelLog = void 0; 16 | const typeorm_1 = require("typeorm"); 17 | const DuelLogPlayer_1 = require("./DuelLogPlayer"); 18 | const moment_1 = __importDefault(require("moment")); 19 | const underscore_1 = __importDefault(require("underscore")); 20 | const CreateAndUpdateTimeBase_1 = require("./CreateAndUpdateTimeBase"); 21 | let DuelLog = class DuelLog extends CreateAndUpdateTimeBase_1.CreateAndUpdateTimeBase { 22 | getViewString() { 23 | const viewPlayers = underscore_1.default.clone(this.players); 24 | viewPlayers.sort((p1, p2) => p1.pos - p2.pos); 25 | const playerString = viewPlayers[0].realName.split("$")[0] + (viewPlayers[2] ? "+" + viewPlayers[2].realName.split("$")[0] : "") + " VS " + (viewPlayers[1] ? viewPlayers[1].realName.split("$")[0] : "AI") + (viewPlayers[3] ? "+" + viewPlayers[3].realName.split("$")[0] : ""); 26 | return `<${this.id}> ${playerString} ${(0, moment_1.default)(this.time).format("YYYY-MM-DD HH-mm-ss")}`; 27 | } 28 | getViewJSON(tournamentModeSettings) { 29 | const data = { 30 | id: this.id, 31 | time: (0, moment_1.default)(this.time).format("YYYY-MM-DD HH:mm:ss"), 32 | name: this.name + (tournamentModeSettings.show_info ? " (Duel:" + this.duelCount + ")" : ""), 33 | roomid: this.roomId, 34 | cloud_replay_id: "R#" + this.cloudReplayId, 35 | replay_filename: this.replayFileName, 36 | roommode: this.roomMode, 37 | players: this.players.map(player => { 38 | return { 39 | pos: player.pos, 40 | is_first: player.isFirst === 1, 41 | name: player.name + (tournamentModeSettings.show_ip ? " (IP: " + player.ip.slice(7) + ")" : "") + (tournamentModeSettings.show_info && !(this.roomMode === 2 && player.pos % 2 > 0) ? " (Score:" + player.score + " LP:" + (player.lp != null ? player.lp : "???") + (this.roomMode !== 2 ? " Cards:" + (player.cardCount != null ? player.cardCount : "???") : "") + ")" : ""), 42 | winner: player.winner === 1 43 | }; 44 | }) 45 | }; 46 | return data; 47 | } 48 | }; 49 | exports.DuelLog = DuelLog; 50 | __decorate([ 51 | (0, typeorm_1.PrimaryGeneratedColumn)({ unsigned: true, type: global.PrimaryKeyType || 'bigint' }), 52 | __metadata("design:type", Number) 53 | ], DuelLog.prototype, "id", void 0); 54 | __decorate([ 55 | (0, typeorm_1.Index)(), 56 | (0, typeorm_1.Column)("datetime"), 57 | __metadata("design:type", Date) 58 | ], DuelLog.prototype, "time", void 0); 59 | __decorate([ 60 | (0, typeorm_1.Index)(), 61 | (0, typeorm_1.Column)({ type: "varchar", length: 20 }), 62 | __metadata("design:type", String) 63 | ], DuelLog.prototype, "name", void 0); 64 | __decorate([ 65 | (0, typeorm_1.Column)("int"), 66 | __metadata("design:type", Number) 67 | ], DuelLog.prototype, "roomId", void 0); 68 | __decorate([ 69 | (0, typeorm_1.Column)(global.PrimaryKeyType || 'bigint'), 70 | __metadata("design:type", Number) 71 | ], DuelLog.prototype, "cloudReplayId", void 0); 72 | __decorate([ 73 | (0, typeorm_1.Column)({ type: "varchar", length: 256 }), 74 | __metadata("design:type", String) 75 | ], DuelLog.prototype, "replayFileName", void 0); 76 | __decorate([ 77 | (0, typeorm_1.Column)("tinyint", { unsigned: true }), 78 | __metadata("design:type", Number) 79 | ], DuelLog.prototype, "roomMode", void 0); 80 | __decorate([ 81 | (0, typeorm_1.Index)(), 82 | (0, typeorm_1.Column)("tinyint", { unsigned: true }), 83 | __metadata("design:type", Number) 84 | ], DuelLog.prototype, "duelCount", void 0); 85 | __decorate([ 86 | (0, typeorm_1.OneToMany)(() => DuelLogPlayer_1.DuelLogPlayer, player => player.duelLog), 87 | __metadata("design:type", Array) 88 | ], DuelLog.prototype, "players", void 0); 89 | exports.DuelLog = DuelLog = __decorate([ 90 | (0, typeorm_1.Entity)({ 91 | orderBy: { 92 | id: "DESC" 93 | } 94 | }) 95 | ], DuelLog); 96 | -------------------------------------------------------------------------------- /msg-polyfill/polyfillers/0x1361.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Polyfiller1361 = exports.gcd = void 0; 4 | const base_polyfiller_1 = require("../base-polyfiller"); 5 | const gcd = (nums) => { 6 | const gcdTwo = (a, b) => { 7 | if (b === 0) 8 | return a; 9 | return gcdTwo(b, a % b); 10 | }; 11 | return nums.reduce((acc, num) => gcdTwo(acc, num)); 12 | }; 13 | exports.gcd = gcd; 14 | class Polyfiller1361 extends base_polyfiller_1.BasePolyfiller { 15 | async polyfillGameMsg(msgTitle, buffer) { 16 | if (msgTitle === 'CONFIRM_CARDS') { 17 | // buf[0]: MSG_CONFIRM_CARDS 18 | // buf[1]: playerid 19 | // buf[2]: ADDED skip_panel 20 | return this.splice(buffer, 2, 1); 21 | } 22 | else if (msgTitle === 'SELECT_CHAIN') { 23 | // buf[0]: MSG_SELECT_CHAIN 24 | // buf[1]: playerid 25 | // buf[2]: size 26 | // buf[3]: spe_count 27 | // buf[REMOVED]: forced 28 | // buf[4-7]: hint_timing player 29 | // buf[8-11]: hint_timing 1-player 30 | // then it's 14 bytes for each item 31 | // item[0]: not related 32 | // item[1]: ADDED forced 33 | // item[2-13] not related 34 | const size = buffer[2]; 35 | const itemStartOffset = 12; // after the header (up to hint timings) 36 | // 判断是否存在任何 item 的 forced = 1(在原始 buffer 中判断) 37 | let anyForced = false; 38 | for (let i = 0; i < size; i++) { 39 | const itemOffset = itemStartOffset + i * 14; 40 | const forced = buffer[itemOffset + 1]; 41 | if (forced === 1) { 42 | anyForced = true; 43 | break; 44 | } 45 | } 46 | // 从后往前 splice 每个 item 的 forced 字段 47 | for (let i = size - 1; i >= 0; i--) { 48 | const itemOffset = itemStartOffset + i * 14; 49 | buffer = this.splice(buffer, itemOffset + 1, 1); // 删除每个 item 的 forced(第 1 字节) 50 | } 51 | // 最后再插入旧版所需的 forced 标志 52 | return this.insert(buffer, 4, Buffer.from([anyForced ? 1 : 0])); 53 | } 54 | else if (msgTitle === 'SELECT_SUM') { 55 | // buf[0]: MSG_SELECT_SUM 56 | // buf[1]: 0 => equal, 1 => greater 57 | // buf[2]: playerid 58 | // buf[3-6]: target_value 59 | // buf[7]: min 60 | // buf[8]: max 61 | // buf[9]: forced_count 62 | // then each item 11 bytes 63 | // item[0-3] code 64 | // item[4] controler 65 | // item[5] location 66 | // item[6] sequence 67 | // item[7-10] value 68 | // item[10 + forced_count * 11] card_count 69 | // same as above items 70 | const targetValue = buffer.readUInt32LE(3); 71 | const forcedCount = buffer[9]; 72 | const cardCount = buffer[10 + forcedCount * 11]; 73 | const valueOffsets = []; 74 | for (let i = 0; i < forcedCount; i++) { 75 | const itemOffset = 10 + i * 11; 76 | valueOffsets.push(itemOffset + 7); 77 | } 78 | for (let i = 0; i < cardCount; i++) { 79 | const itemOffset = 11 + forcedCount * 11 + i * 11; 80 | valueOffsets.push(itemOffset + 7); 81 | } 82 | const values = valueOffsets.map(offset => ({ 83 | offset, 84 | value: buffer.readUInt32LE(offset), 85 | })); 86 | if (!values.some(v => v.value & 0x80000000)) { 87 | return; 88 | } 89 | const gcds = [targetValue]; 90 | for (const { value } of values) { 91 | if (value & 0x80000000) { 92 | gcds.push(value & 0x7FFFFFFF); 93 | } 94 | else { 95 | const op1 = value & 0xffff; 96 | const op2 = (value >>> 16) & 0xffff; 97 | [op1, op2].filter(v => v > 0) 98 | .forEach(v => gcds.push(v)); 99 | } 100 | } 101 | const gcdValue = (0, exports.gcd)(gcds); 102 | buffer.writeUInt32LE(targetValue / gcdValue, 3); 103 | for (const trans of values) { 104 | let target = 0; 105 | const { value } = trans; 106 | if (value & 0x80000000) { 107 | target = ((value & 0x7FFFFFFF) / gcdValue) & 0xffff; 108 | } 109 | else { 110 | // shrink op1 and op2 111 | const op1 = value & 0xffff; 112 | const op2 = (value >>> 16) & 0xffff; 113 | target = ((op1 / gcdValue) & 0xffff) | ((((op2 / gcdValue) & 0xffff) << 16) >>> 0); 114 | } 115 | buffer.writeUInt32LE(target, trans.offset); 116 | } 117 | } 118 | return; 119 | } 120 | } 121 | exports.Polyfiller1361 = Polyfiller1361; 122 | -------------------------------------------------------------------------------- /challonge.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { createLogger } from 'bunyan'; 3 | import moment, { Moment } from 'moment'; 4 | import PQueue from 'p-queue'; 5 | import _ from 'underscore'; 6 | 7 | export interface Match { 8 | id: number; 9 | state: 'pending' | 'open' | 'complete'; // pending: 还未开始,open: 进行中,complete: 已结束 10 | player1_id: number; 11 | player2_id: number; 12 | winner_id?: number | 'tie'; // 如果存在,则代表该比赛已经结束 13 | scores_csv?: string; // 比分,2-1 这样的格式,请保证和上传的情况相同 14 | } 15 | 16 | export interface MatchWrapper { 17 | match: Match; 18 | } 19 | 20 | export interface Participant { 21 | id: number; 22 | name: string; // 玩家的名称,影响玩家的进服匹配 23 | deckbuf?: string; // 玩家的卡组。如果存在,那么卡组由比赛系统管理。base64 24 | // 构造方法: [uint32 maincount+extracount][uint32 sideccount][uint32 card1][uint32 card2]... 25 | // 示例: NwAAAA8AAAC8beUDdgljAnYJYwJ2CWMCEUKKAxFCigOzoLECB1ekBQdXpAUHV6QFPO4FAzzuBQOSZMQEziwNBM4sDQTOLA0EryPeAK8j3gCvI94AKpVlASqVZQEqlWUBTkEDAE5BAwBOQQMAUI+IAFCPiABQj4gA+twUAaab9AGEoUIBwsdyAcLHcgHCx3IBPRWmBSJImQAiSJkAIkiZADdj4QF8oe8FpFt8A5chZAW1XJ8APXyNAMYzYwOIEXYDtfABBavrrQBq4agDn5BqANCkFwEJWmMAWfK5A3OVmwF8e+QD1xqfAdcanwF99r8Affa/AB43ggEeN4IBhCV+AIQlfgCEJX4APqRxAT6kcQE/OuoDb3bvAG927wC0/F4B 26 | } 27 | 28 | export interface ParticipantWrapper { 29 | participant: Participant; 30 | } 31 | 32 | export interface Tournament { 33 | id: number; 34 | participants: ParticipantWrapper[]; 35 | matches: MatchWrapper[]; 36 | } 37 | 38 | // GET /v1/tournaments/${tournament_id}.json?api_key=xxx&include_participants=1&include_matches=1 returns { tournament: Tournament } 39 | export interface TournamentWrapper { 40 | tournament: Tournament; 41 | } 42 | 43 | // PUT /v1/tournaments/${tournament_id}/matches/${match_id}.json { api_key: string, match: MatchPost } returns ANY 44 | export interface MatchPost { 45 | scores_csv: string; // 比分。2-1 这样的格式。可能有特殊情况,比如 -9-1 或者 1--9,代表有一方掉线,或是加时赛胜利。也就是允许负数(从第一串数字的最后一个 - 区分) 46 | winner_id?: number | 'tie'; // 上传比分的时候这个字段不一定存在。如果不存在的话代表比赛没打完(比如 1-0 就会上传,这时候换 side) 47 | } 48 | 49 | export interface ChallongeConfig { 50 | api_key: string; 51 | tournament_id: string; 52 | cache_ttl: number; 53 | challonge_url: string; 54 | } 55 | 56 | export class Challonge { 57 | constructor(private config: ChallongeConfig) { } 58 | 59 | private queue = new PQueue({ concurrency: 1 }) 60 | private log = createLogger({ name: 'challonge' }); 61 | 62 | private previous: Tournament; 63 | private previousTime: Moment; 64 | 65 | private async getTournamentProcess(noCache = false) { 66 | if(!noCache && this.previous && this.previousTime.isAfter(moment().subtract(this.config.cache_ttl, 'ms'))) { 67 | return this.previous; 68 | } 69 | try { 70 | const { data: { tournament } } = await axios.get( 71 | `${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}.json`, 72 | { 73 | params: { 74 | api_key: this.config.api_key, 75 | include_participants: 1, 76 | include_matches: 1, 77 | }, 78 | timeout: 5000, 79 | }, 80 | ); 81 | this.previous = tournament; 82 | this.previousTime = moment(); 83 | return tournament; 84 | } catch (e) { 85 | this.log.error(`Failed to get tournament ${this.config.tournament_id}: ${e}`); 86 | return; 87 | } 88 | } 89 | 90 | async getTournament(noCache = false) { 91 | if (noCache) { 92 | return this.getTournamentProcess(noCache); 93 | } 94 | return this.queue.add(() => this.getTournamentProcess()) 95 | } 96 | 97 | async putScore(matchId: number, match: MatchPost, retried = 0) { 98 | try { 99 | await axios.put( 100 | `${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}/matches/${matchId}.json`, 101 | { 102 | api_key: this.config.api_key, 103 | match: match, 104 | }, 105 | ); 106 | this.previous = undefined; 107 | this.previousTime = undefined; 108 | return true; 109 | } catch (e) { 110 | this.log.error(`Failed to put score for match ${matchId}: ${e}`); 111 | if (retried < 5) { 112 | this.log.info(`Retrying match ${matchId}`); 113 | return this.putScore(matchId, match, retried + 1); 114 | } else { 115 | this.log.error(`Failed to put score for match ${matchId} after 5 retries`); 116 | return false; 117 | } 118 | } 119 | } 120 | 121 | // DELETE /v1/tournaments/${tournament_id}/participants/clear.json?api_key=xxx returns ANY 122 | async clearParticipants() { 123 | try { 124 | await axios.delete(`${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}/participants/clear.json`, { 125 | params: { 126 | api_key: this.config.api_key 127 | }, 128 | validateStatus: () => true, 129 | }) 130 | return true; 131 | } catch (e) { 132 | this.log.error(`Failed to clear participants for tournament ${this.config.tournament_id}: ${e}`); 133 | return false; 134 | } 135 | } 136 | 137 | // POST /v1/tournaments/${tournament_id}/participants/bulk_add.json { api_key: string, participants: { name: string }[] } returns ANY 138 | async uploadParticipants(participantNames: string[]) { 139 | try { 140 | await axios.post(`${this.config.challonge_url}/v1/tournaments/${this.config.tournament_id}/participants/bulk_add.json`, { 141 | api_key: this.config.api_key, 142 | participants: participantNames.map(name => ({ name })), 143 | }); 144 | return true; 145 | } catch (e) { 146 | this.log.error(`Failed to upload participants for tournament ${this.config.tournament_id}: ${e}`); 147 | return false; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /ygopro-auth.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | (function() { 3 | /* 4 | Main script of new dashboard account system. 5 | The account list file is stored at `./config/admin_user.json`. The users are stored at `users`. 6 | The key is the username. The `permissions` field could be a string, using a permission set from the example, or an object, to define a specific set of permissions. 7 | eg. An account for a judge could be as follows, to use the default permission of judges, 8 | "username": { 9 | "password": "123456", 10 | "enabled": true, 11 | "permissions": "judge" 12 | }, 13 | or as follows, to use a specific set of permissions. 14 | "username": { 15 | "password": "123456", 16 | "enabled": true, 17 | "permissions": { 18 | "get_rooms": true, 19 | "duel_log": true, 20 | "download_replay": true, 21 | "deck_dashboard_read": true, 22 | "deck_dashboard_write": true, 23 | "shout": true, 24 | "kick_user": true, 25 | "start_death": true 26 | } 27 | }, 28 | */ 29 | var add_log, bunyan, check_permission, default_data, fs, loadJSON, loadJSONPromise, log, moment, reload, save, setting_save, users, util; 30 | 31 | fs = require('fs'); 32 | 33 | loadJSON = require('load-json-file').sync; 34 | 35 | loadJSONPromise = require('load-json-file'); 36 | 37 | moment = require('moment'); 38 | 39 | moment.updateLocale('zh-cn', { 40 | relativeTime: { 41 | future: '%s内', 42 | past: '%s前', 43 | s: '%d秒', 44 | m: '1分钟', 45 | mm: '%d分钟', 46 | h: '1小时', 47 | hh: '%d小时', 48 | d: '1天', 49 | dd: '%d天', 50 | M: '1个月', 51 | MM: '%d个月', 52 | y: '1年', 53 | yy: '%d年' 54 | } 55 | }); 56 | 57 | bunyan = require('bunyan'); 58 | 59 | log = bunyan.createLogger({ 60 | name: "auth" 61 | }); 62 | 63 | util = require('util'); 64 | 65 | if (!fs.existsSync('./logs')) { 66 | fs.mkdirSync('./logs'); 67 | } 68 | 69 | add_log = async function(message) { 70 | var mt, res, text; 71 | mt = moment(); 72 | log.info(message); 73 | text = mt.format('YYYY-MM-DD HH:mm:ss') + " --> " + message + "\n"; 74 | res = false; 75 | try { 76 | await fs.promises.appendFile("./logs/" + mt.format('YYYY-MM-DD') + ".log", text); 77 | res = true; 78 | } catch (error) { 79 | res = false; 80 | } 81 | return res; 82 | }; 83 | 84 | default_data = loadJSON('./data/default_data.json'); 85 | 86 | setting_save = async function(settings) { 87 | var e; 88 | try { 89 | await fs.promises.writeFile(settings.file, JSON.stringify(settings, null, 2)); 90 | } catch (error) { 91 | e = error; 92 | add_log("save fail"); 93 | } 94 | }; 95 | 96 | default_data = loadJSON('./data/default_data.json'); 97 | 98 | try { 99 | users = loadJSON('./config/admin_user.json'); 100 | } catch (error) { 101 | users = default_data.users; 102 | setting_save(users); 103 | } 104 | 105 | save = async function() { 106 | return (await setting_save(users)); 107 | }; 108 | 109 | reload = async function() { 110 | var user_backup; 111 | user_backup = users; 112 | try { 113 | users = (await loadJSONPromise('./config/admin_user.json')); 114 | } catch (error) { 115 | users = user_backup; 116 | await add_log("Invalid user data JSON"); 117 | } 118 | }; 119 | 120 | check_permission = async function(user, permission_required) { 121 | var _permission, permission; 122 | _permission = user.permissions; 123 | permission = _permission; 124 | if (typeof permission !== 'object') { 125 | permission = users.permission_examples[_permission]; 126 | } 127 | if (!permission) { 128 | await add_log("Permision not set:" + _permission); 129 | return false; 130 | } 131 | return permission[permission_required]; 132 | }; 133 | 134 | this.auth = async function(name, pass, permission_required, action = 'unknown', no_log) { 135 | var user; 136 | await reload(); 137 | user = users.users[name]; 138 | if (!user) { 139 | await add_log("Unknown user login. User: " + name + ", Permission needed: " + permission_required + ", Action: " + action); 140 | return false; 141 | } 142 | if (user.password !== pass) { 143 | await add_log("Unauthorized user login. User: " + name + ", Permission needed: " + permission_required + ", Action: " + action); 144 | return false; 145 | } 146 | if (!user.enabled) { 147 | await add_log("Disabled user login. User: " + name + ", Permission needed: " + permission_required + ", Action: " + action); 148 | return false; 149 | } 150 | if (!(await check_permission(user, permission_required))) { 151 | await add_log("Permission denied. User: " + name + ", Permission needed: " + permission_required + ", Action: " + action); 152 | return false; 153 | } 154 | if (!no_log) { 155 | await add_log("Operation success. User: " + name + ", Permission needed: " + permission_required + ", Action: " + action); 156 | } 157 | return true; 158 | }; 159 | 160 | this.add_user = async function(name, pass, enabled, permissions) { 161 | await reload(); 162 | if (users.users[name]) { 163 | return false; 164 | } 165 | users.users[name] = { 166 | "password": pass, 167 | "enabled": enabled, 168 | "permissions": permissions 169 | }; 170 | await save(); 171 | return true; 172 | }; 173 | 174 | this.delete_user = async function(name) { 175 | await reload(); 176 | if (!users.users[name]) { 177 | return false; 178 | } 179 | delete users.users[name]; 180 | await save(); 181 | }; 182 | 183 | this.update_user = async function(name, key, value) { 184 | await reload(); 185 | if (!users.users[name]) { 186 | return false; 187 | } 188 | users.users[name][key] = value; 189 | await save(); 190 | }; 191 | 192 | }).call(this); 193 | -------------------------------------------------------------------------------- /ygopro.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 2.7.0 2 | (function() { 3 | var Struct, YGOProMessagesHelper, _, loadJSON, translateHandler; 4 | 5 | _ = require('underscore'); 6 | 7 | _.str = require('underscore.string'); 8 | 9 | _.mixin(_.str.exports()); 10 | 11 | Struct = require('./struct.js').Struct; 12 | 13 | loadJSON = require('load-json-file').sync; 14 | 15 | this.i18ns = loadJSON('./data/i18n.json'); 16 | 17 | this.i18nR = {}; 18 | 19 | this.reloadI18nR = function() { 20 | var data, key, lang, ref, results, text; 21 | ref = this.i18ns; 22 | results = []; 23 | for (lang in ref) { 24 | data = ref[lang]; 25 | this.i18nR[lang] = {}; 26 | results.push((function() { 27 | var results1; 28 | results1 = []; 29 | for (key in data) { 30 | text = data[key]; 31 | results1.push(this.i18nR[lang][key] = { 32 | regex: new RegExp("\\$\\{" + key + "\\}", 'g'), 33 | text: text 34 | }); 35 | } 36 | return results1; 37 | }).call(this)); 38 | } 39 | return results; 40 | }; 41 | 42 | this.reloadI18nR(); 43 | 44 | YGOProMessagesHelper = require("./YGOProMessages.js").YGOProMessagesHelper; // 为 SRVPro2 准备的库,这里拿这个库只用来测试,SRVPro1 对异步支持不是特别完善,因此不会有很多异步优化 45 | 46 | this.helper = new YGOProMessagesHelper(9000); 47 | 48 | this.structs = this.helper.structs; 49 | 50 | this.structs_declaration = this.helper.structs_declaration; 51 | 52 | this.typedefs = this.helper.typedefs; 53 | 54 | this.proto_structs = this.helper.proto_structs; 55 | 56 | this.constants = this.helper.constants; 57 | 58 | translateHandler = function(handler) { 59 | return async function(buffer, info, datas, params) { 60 | return (await handler(buffer, info, params.client, params.server, datas)); 61 | }; 62 | }; 63 | 64 | this.stoc_follow = function(proto, synchronous, callback) { 65 | this.helper.addHandler(`STOC_${proto}`, translateHandler(callback), synchronous, 1); 66 | }; 67 | 68 | this.stoc_follow_before = function(proto, synchronous, callback) { 69 | this.helper.addHandler(`STOC_${proto}`, translateHandler(callback), synchronous, 0); 70 | }; 71 | 72 | this.stoc_follow_after = function(proto, synchronous, callback) { 73 | this.helper.addHandler(`STOC_${proto}`, translateHandler(callback), synchronous, 2); 74 | }; 75 | 76 | this.ctos_follow = function(proto, synchronous, callback) { 77 | this.helper.addHandler(`CTOS_${proto}`, translateHandler(callback), synchronous, 1); 78 | }; 79 | 80 | this.ctos_follow_before = function(proto, synchronous, callback) { 81 | this.helper.addHandler(`CTOS_${proto}`, translateHandler(callback), synchronous, 0); 82 | }; 83 | 84 | this.ctos_follow_after = function(proto, synchronous, callback) { 85 | this.helper.addHandler(`CTOS_${proto}`, translateHandler(callback), synchronous, 2); 86 | }; 87 | 88 | //消息发送函数,至少要把俩合起来.... 89 | this.stoc_send = function(socket, proto, info) { 90 | return this.helper.sendMessage(socket, `STOC_${proto}`, info); 91 | }; 92 | 93 | this.ctos_send = function(socket, proto, info) { 94 | return this.helper.sendMessage(socket, `CTOS_${proto}`, info); 95 | }; 96 | 97 | //util 98 | this.split_chat_lines = function(msg, player, lang) { 99 | var i, len, line, lines, o, r, ref, ref1; 100 | lines = []; 101 | ref = _.lines(msg); 102 | for (i = 0, len = ref.length; i < len; i++) { 103 | line = ref[i]; 104 | if (player >= 10) { 105 | line = "[Server]: " + line; 106 | } 107 | ref1 = this.i18nR[lang]; 108 | for (o in ref1) { 109 | r = ref1[o]; 110 | line = line.replace(r.regex, r.text); 111 | } 112 | lines.push(line); 113 | } 114 | return lines; 115 | }; 116 | 117 | this.stoc_send_chat = async function(client, msg, player = 8) { 118 | var i, len, line, ref; 119 | if (!client) { 120 | console.log("err stoc_send_chat"); 121 | return; 122 | } 123 | ref = this.split_chat_lines(msg, player, client.lang); 124 | for (i = 0, len = ref.length; i < len; i++) { 125 | line = ref[i]; 126 | await this.stoc_send(client, 'CHAT', { 127 | player: player, 128 | msg: line 129 | }); 130 | } 131 | }; 132 | 133 | this.stoc_send_chat_to_room = function(room, msg, player = 8) { 134 | var client, i, j, len, len1, ref, ref1; 135 | if (!room) { 136 | console.log("err stoc_send_chat_to_room"); 137 | return; 138 | } 139 | ref = room.players; 140 | for (i = 0, len = ref.length; i < len; i++) { 141 | client = ref[i]; 142 | if (client) { 143 | this.stoc_send_chat(client, msg, player); 144 | } 145 | } 146 | ref1 = room.watchers; 147 | for (j = 0, len1 = ref1.length; j < len1; j++) { 148 | client = ref1[j]; 149 | if (client) { 150 | this.stoc_send_chat(client, msg, player); 151 | } 152 | } 153 | room.recordChatMessage(msg, player); 154 | }; 155 | 156 | this.stoc_send_hint_card_to_room = function(room, card) { 157 | var client, i, j, len, len1, ref, ref1; 158 | if (!room) { 159 | console.log("err stoc_send_hint_card_to_room"); 160 | return; 161 | } 162 | ref = room.players; 163 | for (i = 0, len = ref.length; i < len; i++) { 164 | client = ref[i]; 165 | if (client) { 166 | this.stoc_send(client, 'GAME_MSG', { 167 | curmsg: 2, 168 | type: 10, 169 | player: 0, 170 | data: card 171 | }); 172 | } 173 | } 174 | ref1 = room.watchers; 175 | for (j = 0, len1 = ref1.length; j < len1; j++) { 176 | client = ref1[j]; 177 | if (client) { 178 | this.stoc_send(client, 'GAME_MSG', { 179 | curmsg: 2, 180 | type: 10, 181 | player: 0, 182 | data: card 183 | }); 184 | } 185 | } 186 | }; 187 | 188 | this.stoc_die = async function(client, msg) { 189 | await this.stoc_send_chat(client, msg, this.constants.COLORS.RED); 190 | if (client) { 191 | await this.stoc_send(client, 'ERROR_MSG', { 192 | msg: 1, 193 | code: 9 194 | }); 195 | } 196 | if (client) { 197 | client.system_kicked = true; 198 | client.destroy(); 199 | } 200 | return '_cancel'; 201 | }; 202 | 203 | }).call(this); 204 | -------------------------------------------------------------------------------- /ygopro-webhook.js: -------------------------------------------------------------------------------- 1 | /* 2 | ygopro-webhook.js 3 | ygopro webhook auto update 4 | 5 | To use this, add a webhook of http://(server_ip):(port)/api/(password)/(repo) into the related github repos. 6 | eg. Set http://tiramisu.mycard.moe:7966/api/123456/script in ygopro-scripts to make the server script synced with github FOREVER. 7 | 8 | Author: Nanahira 9 | License: MIT 10 | 11 | */ 12 | var http = require('http'); 13 | var https = require('https'); 14 | var fs = require('fs'); 15 | var execSync = require('child_process').execSync; 16 | var spawn = require('child_process').spawn; 17 | var spawnSync = require('child_process').spawnSync; 18 | var url = require('url'); 19 | var moment = require('moment'); 20 | moment.locale('zh-cn'); 21 | var loadJSON = require('load-json-file').sync; 22 | 23 | //var constants = loadJSON('./data/constants.json'); 24 | 25 | var settings = loadJSON('./config/config.json'); 26 | config = settings.modules.webhook; 27 | ssl_config = settings.modules.http.ssl; 28 | 29 | var status = {}; 30 | 31 | var sendResponse = function(text) { 32 | console.log(moment().format('YYYY-MM-DD HH:mm:ss') + " --> " + text); 33 | } 34 | 35 | var pull_data = function(path, remote, branch, callback) { 36 | sendResponse("Started pulling on branch "+branch+" at "+path+" from "+remote+"."); 37 | try { 38 | var proc = spawn("git", ["pull", remote, branch], { cwd: path, env: process.env }); 39 | proc.stdout.setEncoding('utf8'); 40 | proc.stdout.on('data', function(data) { 41 | sendResponse("git pull stdout: "+data); 42 | }); 43 | proc.stderr.setEncoding('utf8'); 44 | proc.stderr.on('data', function(data) { 45 | sendResponse("git pull stderr: "+data); 46 | }); 47 | proc.on('close', function (code) { 48 | sendResponse("Finished pulling on branch "+branch+" at "+path+" from "+remote+"."); 49 | if (callback) { 50 | callback(false); 51 | } 52 | }); 53 | } catch (err) { 54 | sendResponse("Errored pulling on branch "+branch+" at "+path+" from "+remote+"."); 55 | if (callback) { 56 | callback(true); 57 | } 58 | } 59 | return; 60 | } 61 | 62 | var reset_repo = function(path, callback) { 63 | sendResponse("Started resetting at "+path+"."); 64 | try { 65 | var proc = spawn("git", ["reset", "--hard", "FETCH_HEAD"], { cwd: path, env: process.env }); 66 | proc.stdout.setEncoding('utf8'); 67 | proc.stdout.on('data', function(data) { 68 | sendResponse("git reset stdout: "+data); 69 | }); 70 | proc.stderr.setEncoding('utf8'); 71 | proc.stderr.on('data', function(data) { 72 | sendResponse("git reset stderr: "+data); 73 | }); 74 | proc.on('close', function (code) { 75 | sendResponse("Finished resetting at "+path+"."); 76 | if (callback) { 77 | callback(false); 78 | } 79 | }); 80 | } catch (err) { 81 | sendResponse("Errored resetting at "+path+"."); 82 | if (callback) { 83 | callback(true); 84 | } 85 | } 86 | return; 87 | } 88 | 89 | var run_custom_callback = function(command, args, path, callback) { 90 | sendResponse("Started running custom callback."); 91 | try { 92 | var proc = spawn(command, args, { cwd: path, env: process.env }); 93 | proc.stdout.setEncoding('utf8'); 94 | proc.stdout.on('data', function(data) { 95 | sendResponse("custom callback stdout: "+data); 96 | }); 97 | proc.stderr.setEncoding('utf8'); 98 | proc.stderr.on('data', function(data) { 99 | sendResponse("custom callback stderr: "+data); 100 | }); 101 | proc.on('close', function (code) { 102 | sendResponse("Finished running custom callback."); 103 | if (callback) { 104 | callback(); 105 | } 106 | }); 107 | } catch (err) { 108 | sendResponse("Errored running custom callback."); 109 | if (callback) { 110 | callback(); 111 | } 112 | } 113 | return; 114 | } 115 | 116 | var pull_callback = function(name, info) { 117 | if (info.forced) { 118 | reset_repo(info.path, function(fail) { 119 | reset_callback(name, info); 120 | }); 121 | } else { 122 | reset_callback(name, info); 123 | } 124 | return; 125 | } 126 | 127 | var reset_callback = function(name, info) { 128 | if (info.callback) { 129 | run_custom_callback(info.callback.command, info.callback.args, info.callback.path, function(fail) { 130 | process_callback(name, info); 131 | }); 132 | } else { 133 | process_callback(name, info); 134 | } 135 | return; 136 | } 137 | 138 | var process_callback = function(name, info) { 139 | if (status[name] === 2) { 140 | status[name] = 1; 141 | sendResponse("The Process "+name+" was triggered during running. It will be ran again."); 142 | pull_data(info.path, info.remote, info.branch, function(fail) { 143 | pull_callback(name, info); 144 | }); 145 | } else { 146 | status[name] = false; 147 | sendResponse("Finished process "+name+"."); 148 | } 149 | return; 150 | } 151 | 152 | var add_process = function(name, info) { 153 | if (status[name]) { 154 | status[name] = 2; 155 | return "Another process in webhook "+name+" is running. The process will start after this."; 156 | } 157 | status[name] = 1; 158 | pull_data(info.path, info.remote, info.branch, function(fail) { 159 | pull_callback(name, info); 160 | }); 161 | return "Started a process in webhook "+name+"."; 162 | } 163 | 164 | //returns 165 | var return_error = function(res, msg) { 166 | res.writeHead(403); 167 | res.end(msg); 168 | sendResponse("Remote error: "+msg); 169 | return; 170 | } 171 | 172 | var return_success = function(res, msg) { 173 | res.writeHead(200); 174 | res.end(msg); 175 | sendResponse("Remote message: "+msg); 176 | return; 177 | } 178 | 179 | var requestListener = function (req, res) { 180 | var u = url.parse(req.url, true); 181 | var data = u.pathname.split("/"); 182 | if (data[0] !== "" || data[1] !== "api") { 183 | return return_error(res, "Invalid format."); 184 | } 185 | if (data[2] !== config.password) { 186 | return return_error(res, "Auth Failed."); 187 | } 188 | var hook = data[3]; 189 | if (!hook) { 190 | return return_error(res, "Invalid format."); 191 | } 192 | var hook_info = config.hooks[hook]; 193 | if (!hook_info) { 194 | return return_error(res, "Webhook "+hook+" not found."); 195 | } 196 | var info = ""; 197 | req.setEncoding("utf8"); 198 | req.addListener('data', function(chunk) { 199 | info += chunk; 200 | }); 201 | req.addListener('end', function() { 202 | var infodata; 203 | try { 204 | infodata = JSON.parse(info); 205 | } catch (err) { 206 | return return_error(res, "Error parsing JSON in webhook "+hook+": " + err); 207 | } 208 | var ref = infodata.ref; 209 | if (!ref) { 210 | return return_success(res, "Not a push trigger in webhook "+hook+". Skipped."); 211 | } 212 | var branch = ref.split("/")[2]; 213 | if (!branch) { 214 | return return_error(res, "Invalid branch."); 215 | } else if (branch !== hook_info.branch) { 216 | return return_success(res, "Branch "+branch+" in webhook "+hook+" is not the current branch "+hook_info.branch+". Skipped."); 217 | } else { 218 | var return_msg = add_process(hook, hook_info); 219 | return return_success(res, return_msg); 220 | } 221 | }); 222 | return; 223 | } 224 | 225 | //will create an http server to receive apis 226 | if (ssl_config.enabled) { 227 | const ssl_cert = fs.readFileSync(ssl_config.cert); 228 | const ssl_key = fs.readFileSync(ssl_config.key); 229 | const options = { 230 | cert: ssl_cert, 231 | key: ssl_key 232 | } 233 | https.createServer(options, requestListener).listen(config.port); 234 | } else { 235 | http.createServer(requestListener).listen(config.port); 236 | } 237 | -------------------------------------------------------------------------------- /ygopro-deck-stats.js: -------------------------------------------------------------------------------- 1 | /* 2 | ygopro-deck-stats.js 3 | get card usage from decks 4 | Author: mercury233 5 | License: MIT 6 | 7 | 读取指定文件夹里所有卡组,得出卡片使用量,生成csv 8 | */ 9 | var sqlite3 = require('sqlite3').verbose(); 10 | var fs = require('fs'); 11 | var loadJSON = require('load-json-file').sync; 12 | var config = loadJSON('./config/deckstats.json'); //{ "deckpath": "../decks", "dbfile": "cards.cdb" } 13 | var constants = loadJSON('./data/constants.json'); 14 | 15 | var ALL_MAIN_CARDS={}; 16 | var ALL_SIDE_CARDS={}; 17 | var ALL_CARD_DATAS={}; 18 | 19 | function add_to_deck(deck,id) { 20 | if (deck[id]) { 21 | deck[id]=deck[id]+1; 22 | } 23 | else { 24 | deck[id]=1; 25 | } 26 | } 27 | 28 | function add_to_all_list(LIST,id,use) { 29 | if (!ALL_CARD_DATAS[id]) { 30 | return; 31 | } 32 | if (ALL_CARD_DATAS[id].alias) { 33 | id=ALL_CARD_DATAS[id].alias; 34 | } 35 | if (!LIST[id]) { 36 | LIST[id]={"use1":0, "use2":0, "use3":0}; 37 | } 38 | if (use==1) { 39 | LIST[id].use1=LIST[id].use1+1; 40 | } 41 | else if (use==2) { 42 | LIST[id].use2=LIST[id].use2+1; 43 | } 44 | else { 45 | LIST[id].use3=LIST[id].use3+1; 46 | } 47 | } 48 | 49 | function read_deck_file(filename) { 50 | console.log("reading "+filename); 51 | var deck_text=fs.readFileSync(config.deckpath+"/"+filename,{encoding:"ASCII"}) 52 | var deck_array=deck_text.split("\n"); 53 | var deck_main={}; 54 | var deck_side={}; 55 | var current_deck=deck_main; 56 | for (var i in deck_array) { 57 | if (deck_array[i].indexOf("!side")>=0) 58 | current_deck=deck_side; 59 | var card=parseInt(deck_array[i]); 60 | if (!isNaN(card)) { 61 | add_to_deck(current_deck,card); 62 | } 63 | } 64 | for (var i in deck_main) { 65 | add_to_all_list(ALL_MAIN_CARDS,i,deck_main[i]); 66 | } 67 | for (var i in deck_side) { 68 | add_to_all_list(ALL_SIDE_CARDS,i,deck_side[i]); 69 | } 70 | } 71 | 72 | function load_database(callback) { 73 | var db=new sqlite3.Database(config.dbfile); 74 | db.each("select * from datas,texts where datas.id=texts.id", function (err,result) { 75 | if (err) { 76 | console.log(config.dbfile + ":" + err); 77 | return; 78 | } 79 | else { 80 | if (result.type & constants.TYPES.TYPE_TOKEN) { 81 | return; 82 | } 83 | 84 | var card={}; 85 | 86 | card.name=result.name; 87 | card.alias=result.alias; 88 | 89 | if (result.type & constants.TYPES.TYPE_MONSTER) { 90 | if ((result.type & constants.TYPES.TYPE_FUSION) || (result.type & constants.TYPES.TYPE_SYNCHRO) || (result.type & constants.TYPES.TYPE_XYZ) || (result.type & constants.TYPES.TYPE_LINK)) 91 | card.type="额外"; 92 | else 93 | card.type="怪兽"; 94 | } 95 | if (result.type & constants.TYPES.TYPE_SPELL){ 96 | card.type="魔法"; 97 | } 98 | if (result.type & constants.TYPES.TYPE_TRAP){ 99 | card.type="陷阱"; 100 | } 101 | 102 | var cardTypes=[]; 103 | if (result.type & constants.TYPES.TYPE_MONSTER) {cardTypes.push("怪兽");} 104 | if (result.type & constants.TYPES.TYPE_SPELL) {cardTypes.push("魔法");} 105 | if (result.type & constants.TYPES.TYPE_TRAP) {cardTypes.push("陷阱");} 106 | if (result.type & constants.TYPES.TYPE_NORMAL) {cardTypes.push("通常");} 107 | if (result.type & constants.TYPES.TYPE_EFFECT) {cardTypes.push("效果");} 108 | if (result.type & constants.TYPES.TYPE_FUSION) {cardTypes.push("融合");} 109 | if (result.type & constants.TYPES.TYPE_RITUAL) {cardTypes.push("仪式");} 110 | if (result.type & constants.TYPES.TYPE_TRAPMONSTER) {cardTypes.push("陷阱怪兽");} 111 | if (result.type & constants.TYPES.TYPE_SPIRIT) {cardTypes.push("灵魂");} 112 | if (result.type & constants.TYPES.TYPE_UNION) {cardTypes.push("同盟");} 113 | if (result.type & constants.TYPES.TYPE_DUAL) {cardTypes.push("二重");} 114 | if (result.type & constants.TYPES.TYPE_TUNER) {cardTypes.push("调整");} 115 | if (result.type & constants.TYPES.TYPE_SYNCHRO) {cardTypes.push("同调");} 116 | if (result.type & constants.TYPES.TYPE_TOKEN) {cardTypes.push("衍生物");} 117 | if (result.type & constants.TYPES.TYPE_QUICKPLAY) {cardTypes.push("速攻");} 118 | if (result.type & constants.TYPES.TYPE_CONTINUOUS) {cardTypes.push("永续");} 119 | if (result.type & constants.TYPES.TYPE_EQUIP) {cardTypes.push("装备");} 120 | if (result.type & constants.TYPES.TYPE_FIELD) {cardTypes.push("场地");} 121 | if (result.type & constants.TYPES.TYPE_COUNTER) {cardTypes.push("反击");} 122 | if (result.type & constants.TYPES.TYPE_FLIP) {cardTypes.push("反转");} 123 | if (result.type & constants.TYPES.TYPE_TOON) {cardTypes.push("卡通");} 124 | if (result.type & constants.TYPES.TYPE_XYZ) {cardTypes.push("超量");} 125 | if (result.type & constants.TYPES.TYPE_PENDULUM) {cardTypes.push("灵摆");} 126 | if (result.type & constants.TYPES.TYPE_LINK) {cardTypes.push("连接");} 127 | card.fulltype=cardTypes.join('|'); 128 | 129 | if (result.type & constants.TYPES.TYPE_MONSTER) { 130 | if (result.level<=12) { 131 | card.level=result.level; 132 | } 133 | else { //转化为16位,0x01010004,前2位是左刻度,2-4是右刻度,末2位是等级 134 | var levelHex=parseInt(result.level, 10).toString(16); 135 | card.level=parseInt(levelHex.slice(-2), 16); 136 | card.LScale=parseInt(levelHex.slice(-8,-6), 16); 137 | //card.RScale=parseInt(levelHex.slice(-6,-4), 16); 138 | } 139 | } 140 | 141 | ALL_CARD_DATAS[result.id]=card; 142 | } 143 | }, callback); 144 | } 145 | 146 | function read_decks() { 147 | var ALL_DECKS=fs.readdirSync(config.deckpath); 148 | 149 | for (var i in ALL_DECKS) { 150 | var filename=ALL_DECKS[i]; 151 | if (filename.indexOf(".ydk")>0) { 152 | read_deck_file(filename); 153 | } 154 | } 155 | output_csv(ALL_MAIN_CARDS,"main.csv"); 156 | var ALL_SIDE_CARDS_isempty = true; 157 | for (var j in ALL_SIDE_CARDS) { 158 | ALL_SIDE_CARDS_isempty = false; 159 | break; 160 | } 161 | if (!ALL_SIDE_CARDS_isempty) { 162 | output_csv(ALL_SIDE_CARDS,"side.csv"); 163 | } 164 | } 165 | 166 | function output_csv(list,filename) { 167 | //console.log(JSON.stringify(list)); 168 | var file=fs.openSync(filename,"w"); 169 | for (var i in list) { 170 | var card=ALL_CARD_DATAS[i]; 171 | if (!card) { 172 | continue; 173 | } 174 | var card_usage=list[i]; 175 | 176 | console.log("writing "+card.name); 177 | 178 | var line=[]; 179 | line.push(card.name); 180 | line.push(card.type); 181 | line.push(card.fulltype); 182 | //line.push(card.level ? card.level : ""); 183 | //line.push(card.LScale ? card.LScale : ""); 184 | line.push(card_usage.use1+card_usage.use2+card_usage.use3); 185 | line.push(card_usage.use1); 186 | line.push(card_usage.use2); 187 | line.push(card_usage.use3); 188 | var linetext="\""+line.join("\",\"")+"\"\r\n"; 189 | fs.writeSync(file,linetext); 190 | } 191 | } 192 | 193 | load_database(read_decks); 194 | 195 | -------------------------------------------------------------------------------- /data/constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "NETWORK": { 3 | "29736": "SERVER_ID", 4 | "57078": "CLIENT_ID" 5 | }, 6 | "NETPLAYER": { 7 | "0": "TYPE_PLAYER1", 8 | "1": "TYPE_PLAYER2", 9 | "2": "TYPE_PLAYER3", 10 | "3": "TYPE_PLAYER4", 11 | "4": "TYPE_PLAYER5", 12 | "5": "TYPE_PLAYER6", 13 | "7": "TYPE_OBSERVER" 14 | }, 15 | "CTOS": { 16 | "1": "RESPONSE", 17 | "2": "UPDATE_DECK", 18 | "3": "HAND_RESULT", 19 | "4": "TP_RESULT", 20 | "16": "PLAYER_INFO", 21 | "17": "CREATE_GAME", 22 | "18": "JOIN_GAME", 23 | "19": "LEAVE_GAME", 24 | "20": "SURRENDER", 25 | "21": "TIME_CONFIRM", 26 | "22": "CHAT", 27 | "23": "EXTERNAL_ADDRESS", 28 | "32": "HS_TODUELIST", 29 | "33": "HS_TOOBSERVER", 30 | "34": "HS_READY", 31 | "35": "HS_NOTREADY", 32 | "36": "HS_KICK", 33 | "37": "HS_START", 34 | "48": "REQUEST_FIELD" 35 | }, 36 | "STOC": { 37 | "1": "GAME_MSG", 38 | "2": "ERROR_MSG", 39 | "3": "SELECT_HAND", 40 | "4": "SELECT_TP", 41 | "5": "HAND_RESULT", 42 | "6": "TP_RESULT", 43 | "7": "CHANGE_SIDE", 44 | "8": "WAITING_SIDE", 45 | "9": "DECK_COUNT", 46 | "17": "CREATE_GAME", 47 | "18": "JOIN_GAME", 48 | "19": "TYPE_CHANGE", 49 | "20": "LEAVE_GAME", 50 | "21": "DUEL_START", 51 | "22": "DUEL_END", 52 | "23": "REPLAY", 53 | "24": "TIME_LIMIT", 54 | "25": "CHAT", 55 | "32": "HS_PLAYER_ENTER", 56 | "33": "HS_PLAYER_CHANGE", 57 | "34": "HS_WATCH_CHANGE", 58 | "35": "TEAMMATE_SURRENDER", 59 | "48": "FIELD_FINISH" 60 | }, 61 | "PLAYERCHANGE":{ 62 | "8": "OBSERVE", 63 | "9": "READY", 64 | "10": "NOTREADY", 65 | "11": "LEAVE" 66 | }, 67 | "ERRMSG": { 68 | "1": "JOINERROR", 69 | "2": "DECKERROR", 70 | "3": "SIDEERROR", 71 | "4": "VERERROR" 72 | }, 73 | "MODE": { 74 | "0": "SINGLE", 75 | "1": "MATCH", 76 | "2": "TAG" 77 | }, 78 | "MSG": { 79 | "1": "RETRY", 80 | "2": "HINT", 81 | "3": "WAITING", 82 | "4": "START", 83 | "5": "WIN", 84 | "6": "UPDATE_DATA", 85 | "7": "UPDATE_CARD", 86 | "8": "REQUEST_DECK", 87 | "10": "SELECT_BATTLECMD", 88 | "11": "SELECT_IDLECMD", 89 | "12": "SELECT_EFFECTYN", 90 | "13": "SELECT_YESNO", 91 | "14": "SELECT_OPTION", 92 | "15": "SELECT_CARD", 93 | "16": "SELECT_CHAIN", 94 | "18": "SELECT_PLACE", 95 | "19": "SELECT_POSITION", 96 | "20": "SELECT_TRIBUTE", 97 | "21": "SORT_CHAIN", 98 | "22": "SELECT_COUNTER", 99 | "23": "SELECT_SUM", 100 | "24": "SELECT_DISFIELD", 101 | "25": "SORT_CARD", 102 | "26": "SELECT_UNSELECT_CARD", 103 | "30": "CONFIRM_DECKTOP", 104 | "31": "CONFIRM_CARDS", 105 | "32": "SHUFFLE_DECK", 106 | "33": "SHUFFLE_HAND", 107 | "34": "REFRESH_DECK", 108 | "35": "SWAP_GRAVE_DECK", 109 | "36": "SHUFFLE_SET_CARD", 110 | "37": "REVERSE_DECK", 111 | "38": "DECK_TOP", 112 | "39": "MSG_SHUFFLE_EXTRA", 113 | "40": "NEW_TURN", 114 | "41": "NEW_PHASE", 115 | "42": "CONFIRM_EXTRATOP", 116 | "50": "MOVE", 117 | "53": "POS_CHANGE", 118 | "54": "SET", 119 | "55": "SWAP", 120 | "56": "FIELD_DISABLED", 121 | "60": "SUMMONING", 122 | "61": "SUMMONED", 123 | "62": "SPSUMMONING", 124 | "63": "SPSUMMONED", 125 | "64": "FLIPSUMMONING", 126 | "65": "FLIPSUMMONED", 127 | "70": "CHAINING", 128 | "71": "CHAINED", 129 | "72": "CHAIN_SOLVING", 130 | "73": "CHAIN_SOLVED", 131 | "74": "CHAIN_END", 132 | "75": "CHAIN_NEGATED", 133 | "76": "CHAIN_DISABLED", 134 | "80": "CARD_SELECTED", 135 | "81": "RANDOM_SELECTED", 136 | "83": "BECOME_TARGET", 137 | "90": "DRAW", 138 | "91": "DAMAGE", 139 | "92": "RECOVER", 140 | "93": "EQUIP", 141 | "94": "LPUPDATE", 142 | "95": "UNEQUIP", 143 | "96": "CARD_TARGET", 144 | "97": "CANCEL_TARGET", 145 | "100": "PAY_LPCOST", 146 | "101": "ADD_COUNTER", 147 | "102": "REMOVE_COUNTER", 148 | "110": "ATTACK", 149 | "111": "BATTLE", 150 | "112": "ATTACK_DISABLED", 151 | "113": "DAMAGE_STEP_START", 152 | "114": "DAMAGE_STEP_END", 153 | "120": "MISSED_EFFECT", 154 | "121": "BE_CHAIN_TARGET", 155 | "122": "CREATE_RELATION", 156 | "123": "RELEASE_RELATION", 157 | "130": "TOSS_COIN", 158 | "131": "TOSS_DICE", 159 | "132": "ROCK_PAPER_SCISSORS", 160 | "133": "HAND_RES", 161 | "140": "ANNOUNCE_RACE", 162 | "141": "ANNOUNCE_ATTRIB", 163 | "142": "ANNOUNCE_CARD", 164 | "143": "ANNOUNCE_NUMBER", 165 | "160": "CARD_HINT", 166 | "161": "TAG_SWAP", 167 | "162": "RELOAD_FIELD", 168 | "163": "AI_NAME", 169 | "164": "SHOW_HINT", 170 | "170": "MATCH_KILL", 171 | "180": "CUSTOM_MSG" 172 | }, 173 | "TIMING":{ 174 | "1": "DRAW_PHASE", 175 | "2": "STANDBY_PHASE", 176 | "4": "MAIN_END", 177 | "8": "BATTLE_START", 178 | "16": "BATTLE_END", 179 | "32": "END_PHASE", 180 | "64": "SUMMON", 181 | "128": "SPSUMMON", 182 | "256": "FLIPSUMMON", 183 | "512": "MSET", 184 | "1024": "SSET", 185 | "2048": "POS_CHANGE", 186 | "4096": "ATTACK", 187 | "8192": "DAMAGE_STEP", 188 | "16384": "DAMAGE_CAL", 189 | "32768": "CHAIN_END", 190 | "65536": "DRAW", 191 | "131072": "DAMAGE", 192 | "262144": "RECOVER", 193 | "524288": "DESTROY", 194 | "1048576": "REMOVE", 195 | "2097152": "TOHAND", 196 | "4194304": "TODECK", 197 | "8388608": "TOGRAVE", 198 | "16777216": "BATTLE_PHASE", 199 | "33554432": "EQUIP" 200 | }, 201 | "TYPES": { 202 | "TYPE_MONSTER": 1, 203 | "TYPE_SPELL": 2, 204 | "TYPE_TRAP": 4, 205 | "TYPE_NORMAL": 16, 206 | "TYPE_EFFECT": 32, 207 | "TYPE_FUSION": 64, 208 | "TYPE_RITUAL": 128, 209 | "TYPE_TRAPMONSTER": 256, 210 | "TYPE_SPIRIT": 512, 211 | "TYPE_UNION": 1024, 212 | "TYPE_DUAL": 2048, 213 | "TYPE_TUNER": 4096, 214 | "TYPE_SYNCHRO": 8192, 215 | "TYPE_TOKEN": 16384, 216 | "TYPE_QUICKPLAY": 65536, 217 | "TYPE_CONTINUOUS": 131072, 218 | "TYPE_EQUIP": 262144, 219 | "TYPE_FIELD": 524288, 220 | "TYPE_COUNTER": 1048576, 221 | "TYPE_FLIP": 2097152, 222 | "TYPE_TOON": 4194304, 223 | "TYPE_XYZ": 8388608, 224 | "TYPE_PENDULUM": 16777216, 225 | "TYPE_SPSUMMON": 33554432, 226 | "TYPE_LINK": 67108864 227 | }, 228 | "RACES": { 229 | "RACE_WARRIOR": 1, 230 | "RACE_SPELLCASTER": 2, 231 | "RACE_FAIRY": 4, 232 | "RACE_FIEND": 8, 233 | "RACE_ZOMBIE": 16, 234 | "RACE_MACHINE": 32, 235 | "RACE_AQUA": 64, 236 | "RACE_PYRO": 128, 237 | "RACE_ROCK": 256, 238 | "RACE_WINDBEAST": 512, 239 | "RACE_PLANT": 1024, 240 | "RACE_INSECT": 2048, 241 | "RACE_THUNDER": 4096, 242 | "RACE_DRAGON": 8192, 243 | "RACE_BEAST": 16384, 244 | "RACE_BEASTWARRIOR": 32768, 245 | "RACE_DINOSAUR": 65536, 246 | "RACE_FISH": 131072, 247 | "RACE_SEASERPENT": 262144, 248 | "RACE_REPTILE": 524288, 249 | "RACE_PSYCHO": 1048576, 250 | "RACE_DEVINE": 2097152, 251 | "RACE_CREATORGOD": 4194304, 252 | "RACE_WYRM": 8388608, 253 | "RACE_CYBERS": 16777216, 254 | "RACE_ILLUSION": 33554432 255 | }, 256 | "ATTRIBUTES": { 257 | "ATTRIBUTE_EARTH": 1, 258 | "ATTRIBUTE_WATER": 2, 259 | "ATTRIBUTE_FIRE": 4, 260 | "ATTRIBUTE_WIND": 8, 261 | "ATTRIBUTE_LIGHT": 16, 262 | "ATTRIBUTE_DARK": 32, 263 | "ATTRIBUTE_DEVINE": 64 264 | }, 265 | "LINK_MARKERS": { 266 | "LINK_MARKER_BOTTOM_LEFT": 1, 267 | "LINK_MARKER_BOTTOM": 2, 268 | "LINK_MARKER_BOTTOM_RIGHT": 4, 269 | "LINK_MARKER_LEFT": 8, 270 | "LINK_MARKER_RIGHT": 32, 271 | "LINK_MARKER_TOP_LEFT": 64, 272 | "LINK_MARKER_TOP": 128, 273 | "LINK_MARKER_TOP_RIGHT": 256 274 | }, 275 | "DUEL_STAGE": { 276 | "BEGIN": 0, 277 | "FINGER": 1, 278 | "FIRSTGO": 2, 279 | "DUELING": 3, 280 | "SIDING": 4, 281 | "END": 5 282 | }, 283 | "COLORS": { 284 | "LIGHTBLUE": 8, 285 | "RED": 11, 286 | "GREEN": 12, 287 | "BLUE": 13, 288 | "BABYBLUE": 14, 289 | "PINK": 15, 290 | "YELLOW": 16, 291 | "WHITE": 17, 292 | "GRAY": 18, 293 | "DARKGRAY": 19 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /struct.js: -------------------------------------------------------------------------------- 1 | // a special version of node-struct by xdenser 2 | // https://github.com/xdenser/node-struct/tree/f843487d6768cd0bf20c2ce7803dde2d92df5694 3 | 4 | function byteField(p, offset) { 5 | this.length = 1; 6 | this.get = function () { 7 | return p.buf[offset]; 8 | } 9 | this.set = function (val) { 10 | p.buf[offset] = val; 11 | } 12 | } 13 | 14 | function boolField(p, offset, length) { 15 | this.length = length; 16 | this.get = function () { 17 | return (p.buf[offset] > 0 ); 18 | } 19 | this.set = function (val) { 20 | p.buf[offset] = val ? 1 : 0; 21 | } 22 | } 23 | 24 | function intField(p, offset, length, le, signed) { 25 | this.length = length; 26 | 27 | function bec(cb) { 28 | for (var i = 0; i < length; i++) 29 | cb(i, length - i - 1); 30 | } 31 | 32 | function lec(cb) { 33 | for (var i = 0; i < length; i++) 34 | cb(i, i); 35 | } 36 | 37 | function getUVal(bor) { 38 | var val = 0; 39 | bor(function (i, o) { 40 | val += Math.pow(256, o) * p.buf[offset + i]; 41 | }) 42 | return val; 43 | } 44 | 45 | function getSVal(bor) { 46 | 47 | var val = getUVal(bor); 48 | if ((p.buf[offset + ( le ? (length - 1) : 0)] & 0x80) == 0x80) { 49 | val -= Math.pow(256, length); 50 | } 51 | return val; 52 | } 53 | 54 | function setVal(bor, val) { 55 | bor(function (i, o) { 56 | p.buf[offset + i] = Math.floor(val / Math.pow(256, o)) & 0xff; 57 | }); 58 | } 59 | 60 | 61 | this.get = function () { 62 | var bor = le ? lec : bec; 63 | return ( signed ? getSVal(bor) : getUVal(bor)); 64 | } 65 | this.set = function (val) { 66 | var bor = le ? lec : bec; 67 | setVal(bor, val); 68 | } 69 | } 70 | 71 | function charField(p, offset, length, encoding) { 72 | this.length = length; 73 | this.encoding = encoding; 74 | this.get = function () { 75 | var result = p.buf.toString(this.encoding, offset, offset + length); 76 | var strlen = result.indexOf("\0"); 77 | if (strlen == -1) { 78 | return result; 79 | } else { 80 | return result.slice(0, strlen); 81 | } 82 | } 83 | this.set = function (val) { 84 | val += "\0"; 85 | if (val.length > length) 86 | val = val.substring(0, length); 87 | p.buf.write(val, offset, this.encoding); 88 | } 89 | } 90 | 91 | function structField(p, offset, struct) { 92 | this.length = struct.length(); 93 | this.get = function () { 94 | return struct; 95 | } 96 | this.set = function (val) { 97 | struct.set(val); 98 | } 99 | this.allocate = function () { 100 | struct._setBuff(p.buf.slice(offset, offset + struct.length())); 101 | } 102 | } 103 | 104 | function arrayField(p, offset, len, type) { 105 | var as = Struct(); 106 | var args = [].slice.call(arguments, 4); 107 | args.unshift(0); 108 | for (var i = 0; i < len; i++) { 109 | if (type instanceof Struct) { 110 | as.struct(i, type.clone()); 111 | } else if (type in as) { 112 | args[0] = i; 113 | as[type].apply(as, args); 114 | } 115 | } 116 | this.length = as.length(); 117 | this.allocate = function () { 118 | as._setBuff(p.buf.slice(offset, offset + as.length())); 119 | } 120 | this.get = function () { 121 | return as; 122 | } 123 | this.set = function (val) { 124 | as.set(val); 125 | } 126 | } 127 | 128 | function Struct() { 129 | if (!(this instanceof Struct)) 130 | return new Struct; 131 | 132 | var priv = { 133 | buf: {}, 134 | allocated: false, 135 | len: 0, 136 | fields: {}, 137 | closures: [] 138 | }, self = this; 139 | 140 | function checkAllocated() { 141 | if (priv.allocated) 142 | throw new Error('Cant change struct after allocation'); 143 | } 144 | 145 | 146 | this.word8 = function (key) { 147 | checkAllocated(); 148 | priv.closures.push(function (p) { 149 | p.fields[key] = new byteField(p, p.len); 150 | p.len++; 151 | }); 152 | return this; 153 | }; 154 | 155 | // Create handlers for various Bool Field Variants 156 | [1, 2, 3, 4].forEach(function (n) { 157 | self['bool' + (n == 1 ? '' : n)] = function (key) { 158 | checkAllocated(); 159 | priv.closures.push(function (p) { 160 | p.fields[key] = new boolField(p, p.len, n); 161 | p.len += n; 162 | }); 163 | return this; 164 | } 165 | }); 166 | 167 | // Create handlers for various Integer Field Variants 168 | [1, 2, 3, 4, 6, 8].forEach(function (n) { 169 | [true, false].forEach(function (le) { 170 | [true, false].forEach(function (signed) { 171 | var name = 'word' + (n * 8) + ( signed ? 'S' : 'U') + ( le ? 'le' : 'be'); 172 | self[name] = function (key) { 173 | checkAllocated(); 174 | priv.closures.push(function (p) { 175 | p.fields[key] = new intField(p, p.len, n, le, signed); 176 | p.len += n; 177 | }); 178 | return this; 179 | }; 180 | }); 181 | }); 182 | }); 183 | 184 | this.chars = function (key, length, encoding) { 185 | checkAllocated(); 186 | priv.closures.push(function (p) { 187 | p.fields[key] = new charField(p, p.len, length, encoding || 'ascii'); 188 | p.len += length; 189 | }); 190 | return this; 191 | } 192 | 193 | this.struct = function (key, struct) { 194 | checkAllocated(); 195 | priv.closures.push(function (p) { 196 | p.fields[key] = new structField(p, p.len, struct.clone()); 197 | p.len += p.fields[key].length; 198 | }); 199 | return this; 200 | } 201 | function construct(constructor, args) { 202 | function F() { 203 | return constructor.apply(this, args); 204 | } 205 | 206 | 207 | F.prototype = constructor.prototype; 208 | return new F(); 209 | } 210 | 211 | 212 | this.array = function (key, length, type) { 213 | checkAllocated(); 214 | var args = [].slice.call(arguments, 1); 215 | args.unshift(null); 216 | args.unshift(null); 217 | priv.closures.push(function (p) { 218 | args[0] = p; 219 | args[1] = p.len; 220 | p.fields[key] = construct(arrayField, args); 221 | p.len += p.fields[key].length; 222 | }); 223 | 224 | return this; 225 | } 226 | var beenHere = false; 227 | 228 | function applyClosures(p) { 229 | if (beenHere) 230 | return; 231 | p.closures.forEach(function (el) { 232 | el(p); 233 | }); 234 | beenHere = true; 235 | } 236 | 237 | function allocateFields() { 238 | for (var key in priv.fields) { 239 | if ('allocate' in priv.fields[key]) 240 | priv.fields[key].allocate(); 241 | } 242 | } 243 | 244 | 245 | this._setBuff = function (buff) { 246 | priv.buf = buff; 247 | applyClosures(priv); 248 | allocateFields(); 249 | priv.allocated = true; 250 | } 251 | 252 | this.allocate = function () { 253 | applyClosures(priv); 254 | priv.buf = Buffer.alloc(priv.len); 255 | allocateFields(); 256 | priv.allocated = true; 257 | return this; 258 | } 259 | 260 | this._getPriv = function () { 261 | return priv; 262 | } 263 | 264 | this.clone = function () { 265 | var c = new Struct; 266 | var p = c._getPriv(); 267 | p.closures = priv.closures; 268 | return c; 269 | } 270 | 271 | this.length = function () { 272 | applyClosures(priv); 273 | return priv.len; 274 | } 275 | 276 | this.get = function (key) { 277 | if (key in priv.fields) { 278 | return priv.fields[key].get(); 279 | } else 280 | throw new Error('Can not find field ' + key); 281 | } 282 | 283 | this.set = function (key, val) { 284 | if (arguments.length == 2) { 285 | if (key in priv.fields) { 286 | priv.fields[key].set(val); 287 | } else 288 | throw new Error('Can not find field ' + key); 289 | } else if (Buffer.isBuffer(key)) { 290 | this._setBuff(key); 291 | } else { 292 | for (var k in key) { 293 | this.set(k, key[k]); 294 | } 295 | } 296 | } 297 | this.buffer = function () { 298 | return priv.buf; 299 | } 300 | 301 | function getFields() { 302 | var fields = {}; 303 | Object.keys(priv.fields).forEach(function (key) { 304 | Object.defineProperty(fields, key, { 305 | get: function () { 306 | var res = self.get(key); 307 | if (res instanceof Struct) return res.fields; 308 | else return res; 309 | }, 310 | set: function (newVal) { 311 | self.set(key, newVal); 312 | }, 313 | enumerable: true 314 | }); 315 | }); 316 | return fields; 317 | }; 318 | 319 | var _fields; 320 | Object.defineProperty(this, 'fields', { 321 | get: function () { 322 | if (_fields) return _fields; 323 | return (_fields = getFields()); 324 | }, 325 | enumerable: true, 326 | configurable: true 327 | }); 328 | 329 | } 330 | exports.Struct = Struct; 331 | -------------------------------------------------------------------------------- /data/default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "file": "./config/config.json", 3 | "port": 7911, 4 | "version": 4945, 5 | "alternative_versions": [], 6 | "hostinfo": { 7 | "lflist": 0, 8 | "rule": 0, 9 | "mode": 0, 10 | "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", 11 | "duel_rule": 5, 12 | "no_check_deck": false, 13 | "no_shuffle_deck": false, 14 | "start_lp": 8000, 15 | "start_hand": 5, 16 | "draw_count": 1, 17 | "time_limit": 180, 18 | "no_watch": false, 19 | "auto_death": false 20 | }, 21 | "modules": { 22 | "welcome": "MyCard YGOPro Server", 23 | "update": "请更新你的客户端版本", 24 | "wait_update": "你的客户端版本高于服务器版本,请等待服务器更新", 25 | "stop": false, 26 | "full": "服务器已爆满", 27 | "max_rooms_count": 0, 28 | "max_mem_percentage": 95, 29 | "side_timeout": false, 30 | "replay_delay": true, 31 | "hide_name": false, 32 | "display_watchers": false, 33 | "trusted_proxies": [ 34 | "127.0.0.1/8", 35 | "::1/128" 36 | ], 37 | "i18n": { 38 | "auto_pick": false, 39 | "default": "zh-cn", 40 | "fallback": "en-us", 41 | "map": { 42 | "CN": "zh-cn", 43 | "HK": "zh-cn", 44 | "MO": "zh-cn", 45 | "TW": "zh-cn", 46 | "JP": "ja-jp", 47 | "AR": "es-es", 48 | "BO": "es-es", 49 | "CL": "es-es", 50 | "CO": "es-es", 51 | "CR": "es-es", 52 | "CU": "es-es", 53 | "EC": "es-es", 54 | "SV": "es-es", 55 | "ES": "es-es", 56 | "GT": "es-es", 57 | "GQ": "es-es", 58 | "HN": "es-es", 59 | "MX": "es-es", 60 | "NI": "es-es", 61 | "PA": "es-es", 62 | "PY": "es-es", 63 | "PE": "es-es", 64 | "DO": "es-es", 65 | "UY": "es-es", 66 | "VE": "es-es", 67 | "KR": "ko-kr" 68 | } 69 | }, 70 | "tips": { 71 | "enabled": true, 72 | "get": false, 73 | "interval": 30000, 74 | "interval_ingame": 120000, 75 | "prefix": "Tip: " 76 | }, 77 | "dialogues": { 78 | "enabled": true, 79 | "get": "http://mercury233.me/ygosrv233/dialogues.json" 80 | }, 81 | "random_duel": { 82 | "enabled": true, 83 | "default_type": "S", 84 | "no_rematch_check": false, 85 | "record_match_scores": false, 86 | "post_match_scores": false, 87 | "post_match_accesskey": "123456", 88 | "disable_chat": false, 89 | "blank_pass_modes": { 90 | "S": true, 91 | "M": true, 92 | "T": false 93 | }, 94 | "ready_time": 20, 95 | "hang_timeout": 90, 96 | "extra_modes": {} 97 | }, 98 | "mysql": { 99 | "enabled": false, 100 | "db": { 101 | "host": "127.0.0.1", 102 | "port": 3306, 103 | "username": "root", 104 | "password": "localhost", 105 | "database": "srvpro" 106 | } 107 | }, 108 | "cloud_replay": { 109 | "enabled": false, 110 | "enable_halfway_watch": true 111 | }, 112 | "windbot": { 113 | "enabled": false, 114 | "botlist": "./windbot/bots.json", 115 | "spawn": false, 116 | "port": 2399, 117 | "server_ip": "127.0.0.1", 118 | "my_ip": "127.0.0.1" 119 | }, 120 | "chat_color": { 121 | "enabled": false 122 | }, 123 | "retry_handle": { 124 | "enabled": true, 125 | "max_retry_count": false 126 | }, 127 | "reconnect": { 128 | "enabled": true, 129 | "auto_surrender_after_disconnect": false, 130 | "allow_kick_reconnect": true, 131 | "wait_time": 90000 132 | }, 133 | "heartbeat_detection": { 134 | "enabled": false, 135 | "interval": 20000, 136 | "wait_time": 10000 137 | }, 138 | "mycard": { 139 | "enabled": false, 140 | "auth_base_url": "https://sapi.moecube.com:444/accounts", 141 | "auth_database": "postgres://233@233.mycard.moe/233", 142 | "ban_get": "https://sapi.moecube.com:444/ygopro/big-brother/ban", 143 | "auth_key": "233333" 144 | }, 145 | "challonge": { 146 | "enabled": false, 147 | "post_detailed_score": true, 148 | "post_score_midduel": true, 149 | "cache_ttl": 60000, 150 | "no_match_mode": false, 151 | "api_key": "123", 152 | "tournament_id": "456", 153 | "challonge_url": "https://api.challonge.com" 154 | }, 155 | "deck_log": { 156 | "enabled": false, 157 | "accesskey": "233", 158 | "local": "./deck_log/", 159 | "post": "https://sapi.moecube.com:444/ygopro/analytics/deck/text", 160 | "arena": "233" 161 | }, 162 | "big_brother": { 163 | "enabled": false, 164 | "accesskey": "233", 165 | "post": "https://sapi.moecube.com:444/ygopro/big-brother" 166 | }, 167 | "arena_mode": { 168 | "enabled": false, 169 | "mode": "entertain", 170 | "comment": "mode: athletic / entertain", 171 | "accesskey": "233", 172 | "ready_time": 30, 173 | "check_permit": "https://sapi.moecube.com:444/ygopro/match/permit", 174 | "post_score": false, 175 | "get_score": false, 176 | "punish_quit_before_match": false, 177 | "init_post": { 178 | "enabled": false, 179 | "url": "https://sapi.moecube.com:444/ygopro/match/clear", 180 | "accesskey": "momobako" 181 | } 182 | }, 183 | "tournament_mode": { 184 | "enabled": false, 185 | "deck_check": true, 186 | "deck_path": "./decks/", 187 | "replay_path": "./replays/", 188 | "replay_archive_tool": "7z", 189 | "block_replay_to_player": false, 190 | "enable_recover": false, 191 | "show_ip": true, 192 | "show_info": true, 193 | "log_save_path": "./config/", 194 | "port": 7933 195 | }, 196 | "athletic_check": { 197 | "enabled": false, 198 | "rankURL": "https://sapi.moecube.com:444/ygopro/analytics/deck/type", 199 | "identifierURL": "https://sapi.moecube.com:444/ygopro/identifier/production", 200 | "athleticFetchParams": { 201 | "type": "week", 202 | "source": "mycard-athletic" 203 | }, 204 | "rankCount": 10, 205 | "banCount": 0, 206 | "ttl": 600 207 | }, 208 | "neos": { 209 | "enabled": false, 210 | "port": 7977, 211 | "ip_header": "x-forwarded-for" 212 | }, 213 | "test_mode": { 214 | "watch_public_hand": false, 215 | "no_connect_count_limit": false, 216 | "no_ban_player": false, 217 | "surrender_anytime": false 218 | }, 219 | "pre_util": { 220 | "enabled": false, 221 | "port": 7944, 222 | "git_html_path": "../mercury233.github.io/", 223 | "html_path": "../mercury233.github.io/ygosrv233/", 224 | "html_filename": "pre.html", 225 | "git_db_path": "../ygopro-pre-data/", 226 | "db_path": "../ygopro-pre-data/unofficial/", 227 | "ypk_name": "pre-release.ypk", 228 | "html_img_rel_path": "pre/pics/", 229 | "html_img_thumbnail": "thumbnail/", 230 | "html_img_thumbnail_suffix": "!thumb", 231 | "cdn": { 232 | "enabled": false, 233 | "exe": "upx", 234 | "params": [ 235 | "sync" 236 | ], 237 | "local": "./ygosrv233", 238 | "remote": "/ygosrv233", 239 | "pics_remote": "/ygopro/" 240 | }, 241 | "ygopro_path": "../ygopro-pre/", 242 | "only_show_dbs": { 243 | "news.cdb": true, 244 | "pre-release.cdb": true 245 | }, 246 | "html_gits": [ 247 | { 248 | "name": "GitHub", 249 | "push": [ 250 | "push", 251 | "origin" 252 | ] 253 | }, 254 | { 255 | "name": "Coding", 256 | "push": [ 257 | "push", 258 | "coding", 259 | "master:master" 260 | ] 261 | } 262 | ] 263 | }, 264 | "webhook": { 265 | "enabled": false, 266 | "port": 7966, 267 | "password": "123456", 268 | "hooks": { 269 | "ygopro": { 270 | "path": "./ygopro/", 271 | "remote": "origin", 272 | "branch": "server", 273 | "forced": false, 274 | "callback": { 275 | "command": "bash", 276 | "args": [ 277 | "-c", 278 | "cd ocgcore ; git pull origin master ; cd ../script ; git pull origin master ; cd .. ; ~/premake5 gmake ; cd build ; make config=release ; cd .. ; strip ygopro" 279 | ], 280 | "path": "./ygopro/" 281 | } 282 | }, 283 | "srvpro": { 284 | "path": ".", 285 | "remote": "origin", 286 | "branch": "master", 287 | "forced": false, 288 | "callback": { 289 | "command": "npm", 290 | "args": [ 291 | "install" 292 | ], 293 | "path": "." 294 | } 295 | } 296 | } 297 | }, 298 | "update_util": { 299 | "enabled": false, 300 | "port": 7955, 301 | "git_html_path": "../ygo233-web/", 302 | "html_path": "../ygo233-web/", 303 | "cdb_path": "./ygopro/cards.cdb", 304 | "script_path": "./ygopro/script", 305 | "changelog_filename": "changelog.json", 306 | "html_gits": [ 307 | { 308 | "name": "GitHub", 309 | "push": [ 310 | "push", 311 | "origin" 312 | ] 313 | }, 314 | { 315 | "name": "Coding", 316 | "push": [ 317 | "push", 318 | "coding", 319 | "master:master" 320 | ] 321 | } 322 | ] 323 | }, 324 | "http": { 325 | "port": 7922, 326 | "websocket_roomlist": false, 327 | "public_roomlist": false, 328 | "show_ip": true, 329 | "show_info": true, 330 | "quick_death_rule": 2, 331 | "ssl": { 332 | "enabled": false, 333 | "port": 7923, 334 | "cert": "ssl/fullchain.pem", 335 | "key": "ssl/privkey.pem" 336 | } 337 | } 338 | }, 339 | "ban": { 340 | "illegal_id": [ 341 | "^Lv\\.-*\\d+\\s*(.*)", 342 | "^VIP\\.\\d+\\s*(.*)" 343 | ], 344 | "spam_word": [ 345 | "——" 346 | ] 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /ygopro-update.js: -------------------------------------------------------------------------------- 1 | /* 2 | ygopro-update.js 3 | ygopro update util (not fully implymented) 4 | Author: mercury233 5 | License: MIT 6 | 7 | 不带参数运行时,会建立一个服务器,调用API执行对应操作 8 | TODO:带参数运行时执行对应操作后退出 9 | */ 10 | var http = require('http'); 11 | var https = require('https'); 12 | var sqlite3 = require('sqlite3').verbose(); 13 | var fs = require('fs'); 14 | var execSync = require('child_process').execSync; 15 | var spawn = require('child_process').spawn; 16 | var spawnSync = require('child_process').spawnSync; 17 | var url = require('url'); 18 | var moment = require('moment'); 19 | moment.locale('zh-cn'); 20 | var loadJSON = require('load-json-file').sync; 21 | 22 | var auth = require('./ygopro-auth.js'); 23 | 24 | var constants = loadJSON('./data/constants.json'); 25 | 26 | var settings = loadJSON('./config/config.json'); 27 | config = settings.modules.update_util; 28 | ssl_config = settings.modules.http.ssl; 29 | 30 | //全卡名称列表 31 | var cardNames={}; 32 | // 33 | var changelog=[]; 34 | //http长连接 35 | var responder; 36 | 37 | //输出反馈信息,如有http长连接则输出到http,否则输出到控制台 38 | var sendResponse = function(text) { 39 | text=""+text; 40 | if (responder) { 41 | text=text.replace(/\n/g,"
"); 42 | responder.write("data: " + text + "\n\n"); 43 | } 44 | else { 45 | console.log(text); 46 | } 47 | } 48 | 49 | //读取数据库内内容到cardNames,异步 50 | var loadDb = function(db_file) { 51 | var db = new sqlite3.Database(db_file); 52 | 53 | db.each("select id,name from texts", function (err,result) { 54 | if (err) { 55 | sendResponse(db_file + ":" + err); 56 | return; 57 | } 58 | else { 59 | cardNames[result.id] = result.name; 60 | } 61 | }, function(err, num) { 62 | if(err) { 63 | sendResponse(db_file + ":" + err); 64 | } 65 | else { 66 | sendResponse("已加载数据库"+db_file+",共"+num+"张卡。"); 67 | } 68 | }); 69 | } 70 | 71 | var loadChangelog = function(json_file) { 72 | changelog = loadJSON(json_file).changelog; 73 | sendResponse("已加载更新记录"+json_file+",共"+changelog.length+"条,最后更新于"+changelog[0].date+"。"); 74 | } 75 | 76 | var makeChangelogs = function(dir, since) { 77 | var lastcommit; 78 | var addedCards=[]; 79 | var changedCards=[]; 80 | var prc_git_log = spawnSync("git", [ "log", "--pretty=%H,%ai", since ], { "cwd" : dir }); 81 | if (prc_git_log.stdout) { 82 | var logs = prc_git_log.stdout.toString().split(/\n/g); 83 | for (var i in logs) { 84 | var log = logs[i].split(","); 85 | var date = log[1]; 86 | if (date) { 87 | var prc_git_diff = spawnSync("git", [ "diff-tree", "--no-commit-id", "--name-only" ,"--diff-filter=A" , "-r", log[0] ], { "cwd" : dir }); 88 | if (prc_git_diff.stdout) { 89 | var lines = prc_git_diff.stdout.toString().split(/\n/g); 90 | for (var j in lines) { 91 | var line = lines[j].match(/c(\d+)\.lua/); 92 | if (line) { 93 | var name = cardNames[line[1]] || line[1]; 94 | addedCards.push(name); 95 | sendResponse("" + date + " + " + name + ""); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | for (var i in logs) { 102 | var log = logs[i].split(","); 103 | var date = log[1]; 104 | if (date) { 105 | var prc_git_diff = spawnSync("git", [ "diff-tree", "--no-commit-id", "--name-only" ,"--diff-filter=CMR" , "-r", log[0] ], { "cwd" : dir }); 106 | if (prc_git_diff.stdout) { 107 | var lines = prc_git_diff.stdout.toString().split(/\n/g); 108 | for (var j in lines) { 109 | var line = lines[j].match(/c(\d+)\.lua/); 110 | if (line) { 111 | var name = cardNames[line[1]] || line[1]; 112 | sendResponse("" + date + " * " + name + ""); 113 | if (!addedCards.includes(name) && !changedCards.includes(name)) { 114 | changedCards.push(name); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | var fullLog = []; 123 | fullLog.push("新增卡片:"); 124 | for (var i in addedCards) { 125 | fullLog.push("- " + addedCards[i]); 126 | } 127 | if (addedCards.length == 0) { 128 | fullLog.push("- 无"); 129 | } 130 | fullLog.push(""); 131 | fullLog.push("卡片更改:"); 132 | for (var i in changedCards) { 133 | fullLog.push("- " + changedCards[i]); 134 | } 135 | if (changedCards.length == 0) { 136 | fullLog.push("- 无"); 137 | } 138 | fullLog.push("\n"); 139 | 140 | var resJSON = {}; 141 | resJSON.type = "changelog"; 142 | resJSON.changelog = fullLog; 143 | sendResponse(JSON.stringify(resJSON)); 144 | } else { 145 | sendResponse("获取更新记录失败:" + prc_git_log.stderr.toString()); 146 | } 147 | } 148 | 149 | //从远程更新数据库,异步 150 | var fetchDatas = function() { 151 | var proc = spawn("git", ["pull", "origin", "master"], { cwd: config.git_html_path, env: process.env }); 152 | proc.stdout.setEncoding('utf8'); 153 | proc.stdout.on('data', function(data) { 154 | sendResponse("git pull: "+data); 155 | }); 156 | proc.stderr.setEncoding('utf8'); 157 | proc.stderr.on('data', function(data) { 158 | sendResponse("git pull: "+data); 159 | }); 160 | proc.on('close', function (code) { 161 | sendResponse("网页同步完成。"); 162 | }); 163 | } 164 | 165 | var updateChangelogs = function(dir, message) { 166 | message = message.split("!换行符!").join("\n"); 167 | change_log = {}; 168 | change_log.title = "服务器更新"; 169 | change_log.date = moment().format("YYYY-MM-DD"); 170 | change_log.text = message; 171 | var prc_git_rev = spawnSync("git", [ "rev-parse", "HEAD" ], { "cwd" : dir }); 172 | if (prc_git_rev.stdout) { 173 | change_log.commit = prc_git_rev.stdout.toString().split(/\n/)[0]; 174 | } 175 | changelog.unshift(change_log); 176 | fileContent = JSON.stringify({ changelog: changelog }, null, 2); 177 | fs.writeFileSync(config.html_path + config.changelog_filename, fileContent); 178 | sendResponse("更新完成,共有" + changelog.length + "条记录。"); 179 | } 180 | 181 | var pushHTMLs = function() { 182 | try { 183 | execSync('git add ' + config.changelog_filename, { cwd: config.git_html_path, env: process.env }); 184 | execSync('git commit -m update-auto', { cwd: config.git_html_path, env: process.env }); 185 | } catch (error) { 186 | sendResponse("git error: "+error.stdout); 187 | } 188 | for (var i in config.html_gits) { 189 | var git = config.html_gits[i]; 190 | var proc = spawn("git", git.push, { cwd: config.git_html_path, env: process.env }); 191 | proc.stdout.setEncoding('utf8'); 192 | proc.stdout.on('data', (function(git) { 193 | return function(data) { 194 | sendResponse(git.name + " git push: " + data); 195 | } 196 | })(git)); 197 | proc.stderr.setEncoding('utf8'); 198 | proc.stderr.on('data', (function(git) { 199 | return function(data) { 200 | sendResponse(git.name + " git push: " + data); 201 | } 202 | })(git)); 203 | proc.on('close', (function(git) { 204 | return function(code) { 205 | sendResponse(git.name + "上传完成。"); 206 | } 207 | })(git)); 208 | } 209 | } 210 | 211 | 212 | //建立一个http服务器,接收API操作 213 | async function requestListener(req, res) { 214 | var u = url.parse(req.url, true); 215 | 216 | if (!await auth.auth(u.query.username, u.query.password, "update_dashboard", "update_dashboard")) { 217 | res.writeHead(403); 218 | res.end("Auth Failed."); 219 | return; 220 | } 221 | 222 | if (u.pathname === '/api/msg') { 223 | res.writeHead(200, { 224 | "Access-Control-Allow-origin": "*", 225 | "Content-Type": "text/event-stream", 226 | "Cache-Control": "no-cache", 227 | "Connection": "keep-alive" 228 | }); 229 | 230 | res.on("close", function(){ 231 | responder = null; 232 | }); 233 | 234 | responder = res; 235 | 236 | sendResponse("已连接。"); 237 | } 238 | else if (u.pathname === '/api/fetch_datas') { 239 | res.writeHead(200); 240 | res.end(u.query.callback+'({"message":"开始更新网页。"});'); 241 | fetchDatas(); 242 | } 243 | else if (u.pathname === '/api/load_db') { 244 | res.writeHead(200); 245 | res.end(u.query.callback+'({"message":"开始加载数据库。"});'); 246 | loadDb(config.cdb_path); 247 | loadChangelog(config.html_path + config.changelog_filename); 248 | } 249 | else if (u.pathname === '/api/make_changelog') { 250 | res.writeHead(200); 251 | var date = moment(changelog[0].date).add(1,'days').format("YYYY-MM-DD"); 252 | res.end(u.query.callback+'({"message":"开始生成'+ date +'以来的更新记录:"});'); 253 | var since = changelog[0].commit ? (changelog[0].commit + "..") : ("--since=" + date); 254 | makeChangelogs(config.script_path, since); 255 | } 256 | else if (u.pathname === '/api/make_more_changelog') { 257 | res.writeHead(200); 258 | res.end(u.query.callback+'({"message":"开始生成最近20次的更新记录:"});'); 259 | makeChangelogs(config.script_path, "-20"); 260 | } 261 | else if (u.pathname === '/api/update_changelog') { 262 | res.writeHead(200); 263 | res.end(u.query.callback+'({"message":"开始写入更新记录。"});'); 264 | updateChangelogs(config.script_path, u.query.message); 265 | } 266 | else if (u.pathname === '/api/push_datas') { 267 | res.writeHead(200); 268 | res.end(u.query.callback+'({"message":"开始上传到网页。"});'); 269 | pushHTMLs(); 270 | } 271 | else { 272 | res.writeHead(400); 273 | res.end("400"); 274 | } 275 | 276 | } 277 | 278 | if (ssl_config.enabled) { 279 | const ssl_cert = fs.readFileSync(ssl_config.cert); 280 | const ssl_key = fs.readFileSync(ssl_config.key); 281 | const options = { 282 | cert: ssl_cert, 283 | key: ssl_key 284 | } 285 | https.createServer(options, requestListener).listen(config.port); 286 | } else { 287 | http.createServer(requestListener).listen(config.port); 288 | } 289 | -------------------------------------------------------------------------------- /YGOProMessages.ts: -------------------------------------------------------------------------------- 1 | import { Struct } from "./struct"; 2 | import _ from "underscore"; 3 | import structs_declaration from "./data/structs.json"; 4 | import typedefs from "./data/typedefs.json"; 5 | import proto_structs from "./data/proto_structs.json"; 6 | import constants from "./data/constants.json"; 7 | import net from "net"; 8 | 9 | 10 | class Handler { 11 | private handler: (buffer: Buffer, info: any, datas: Buffer[], params: any) => Promise; 12 | synchronous: boolean; 13 | constructor(handler: (buffer: Buffer, info: any, datas: Buffer[], params: any) => Promise, synchronous: boolean) { 14 | this.handler = handler; 15 | this.synchronous = synchronous || false; 16 | } 17 | async handle(buffer: Buffer, info: any, datas: Buffer[], params: any): Promise { 18 | if (this.synchronous) { 19 | return await this.handler(buffer, info, datas, params); 20 | } else { 21 | const newBuffer = Buffer.from(buffer); 22 | const newDatas = datas.map(b => Buffer.from(b)); 23 | this.handler(newBuffer, info, newDatas, params); 24 | return false; 25 | } 26 | } 27 | } 28 | 29 | interface HandlerList { 30 | STOC: Map[]; 31 | CTOS: Map[]; 32 | } 33 | 34 | interface DirectionAndProto { 35 | direction: string; 36 | proto: string; 37 | } 38 | 39 | export interface Feedback{ 40 | type: string; 41 | message: string; 42 | } 43 | 44 | export interface HandleResult { 45 | datas: Buffer[]; 46 | feedback: Feedback; 47 | } 48 | 49 | export interface Constants { 50 | TYPES: Record; 51 | RACES: Record; 52 | ATTRIBUTES: Record; 53 | LINK_MARKERS: Record; 54 | DUEL_STAGE: Record; 55 | COLORS: Record; 56 | TIMING: Record; 57 | NETWORK: Record; 58 | NETPLAYER: Record; 59 | CTOS: Record; 60 | STOC: Record; 61 | PLAYERCHANGE: Record; 62 | ERRMSG: Record; 63 | MODE: Record; 64 | MSG: Record; 65 | } 66 | 67 | export class YGOProMessagesHelper { 68 | 69 | handlers: HandlerList; 70 | structs: Map; 71 | structs_declaration: Record; 72 | typedefs: Record; 73 | proto_structs: Record<'CTOS' | 'STOC', Record>; 74 | constants: Constants; 75 | singleHandleLimit: number; 76 | 77 | constructor(singleHandleLimit?: number) { 78 | this.handlers = { 79 | STOC: [new Map(), 80 | new Map(), 81 | new Map(), 82 | new Map(), 83 | new Map(), 84 | ], 85 | CTOS: [new Map(), 86 | new Map(), 87 | new Map(), 88 | new Map(), 89 | new Map(), 90 | ] 91 | } 92 | this.initDatas(); 93 | this.initStructs(); 94 | if (singleHandleLimit) { 95 | this.singleHandleLimit = singleHandleLimit; 96 | } else { 97 | this.singleHandleLimit = 1000; 98 | } 99 | } 100 | 101 | initDatas() { 102 | this.structs_declaration = structs_declaration; 103 | this.typedefs = typedefs; 104 | this.proto_structs = proto_structs; 105 | this.constants = constants; 106 | } 107 | 108 | initStructs() { 109 | this.structs = new Map(); 110 | for (let name in this.structs_declaration) { 111 | const declaration = this.structs_declaration[name]; 112 | let result = Struct(); 113 | for (let field of declaration) { 114 | if (field.encoding) { 115 | switch (field.encoding) { 116 | case "UTF-16LE": 117 | result.chars(field.name, field.length * 2, field.encoding); 118 | break; 119 | default: 120 | throw `unsupported encoding: ${field.encoding}`; 121 | } 122 | } else { 123 | let type = field.type; 124 | if (this.typedefs[type]) { 125 | type = this.typedefs[type]; 126 | } 127 | if (field.length) { 128 | result.array(field.name, field.length, type); //不支持结构体 129 | } else { 130 | if (this.structs.has(type)) { 131 | result.struct(field.name, this.structs.get(type)); 132 | } else { 133 | result[type](field.name); 134 | } 135 | } 136 | } 137 | } 138 | this.structs.set(name, result); 139 | } 140 | } 141 | 142 | getDirectionAndProto(protoStr: string): DirectionAndProto { 143 | const protoStrMatch = protoStr.match(/^(STOC|CTOS)_([_A-Z]+)$/); 144 | if (!protoStrMatch) { 145 | throw `Invalid proto string: ${protoStr}` 146 | } 147 | return { 148 | direction: protoStrMatch[1].toUpperCase(), 149 | proto: protoStrMatch[2].toUpperCase() 150 | } 151 | } 152 | 153 | 154 | translateProto(proto: string | number, direction: string): number { 155 | const directionProtoList = this.constants[direction]; 156 | if (typeof proto !== "string") { 157 | return proto; 158 | } 159 | const translatedProto = _.find(Object.keys(directionProtoList), p => { 160 | return directionProtoList[p] === proto; 161 | }); 162 | if (!translatedProto) { 163 | throw `unknown proto ${direction} ${proto}`; 164 | } 165 | return parseInt(translatedProto); 166 | } 167 | 168 | prepareMessage(protostr: string, info?: string | Buffer | any): Buffer { 169 | const { 170 | direction, 171 | proto 172 | } = this.getDirectionAndProto(protostr); 173 | let buffer: Buffer; 174 | //console.log(proto, this.proto_structs[direction][proto]); 175 | //const directionProtoList = this.constants[direction]; 176 | if (typeof info === 'undefined') { 177 | buffer = null; 178 | } else if (Buffer.isBuffer(info)) { 179 | buffer = info; 180 | } else { 181 | let struct = this.structs.get(this.proto_structs[direction][proto]); 182 | struct.allocate(); 183 | struct.set(info); 184 | buffer = struct.buffer(); 185 | } 186 | const translatedProto = this.translateProto(proto, direction); 187 | let sendBuffer = Buffer.allocUnsafe(3 + (buffer ? buffer.length : 0)); 188 | if (buffer) { 189 | sendBuffer.writeUInt16LE(buffer.length + 1, 0); 190 | sendBuffer.writeUInt8(translatedProto, 2); 191 | buffer.copy(sendBuffer, 3); 192 | } else { 193 | sendBuffer.writeUInt16LE(1, 0); 194 | sendBuffer.writeUInt8(translatedProto, 2); 195 | } 196 | return sendBuffer; 197 | } 198 | 199 | send(socket: net.Socket | WebSocket, buffer: Buffer) { 200 | return new Promise(done => { 201 | if (socket['isWs']) { 202 | const ws = socket as WebSocket; 203 | // @ts-ignore 204 | ws.send(buffer, {}, done); 205 | } else { 206 | const sock = socket as net.Socket; 207 | sock.write(buffer, done); 208 | } 209 | }) 210 | } 211 | 212 | sendMessage(socket: net.Socket | WebSocket, protostr: string, info?: string | Buffer | any): Promise { 213 | const sendBuffer = this.prepareMessage(protostr, info); 214 | return this.send(socket, sendBuffer); 215 | } 216 | 217 | addHandler(protostr: string, handler: (buffer: Buffer, info: any, datas: Buffer[], params: any) => Promise, synchronous: boolean, priority: number) { 218 | if (priority < 0 || priority > 4) { 219 | throw "Invalid priority: " + priority; 220 | } 221 | let { 222 | direction, 223 | proto 224 | } = this.getDirectionAndProto(protostr); 225 | synchronous = synchronous || false; 226 | const handlerObj = new Handler(handler, synchronous); 227 | let handlerCollection: Map = this.handlers[direction][priority]; 228 | const translatedProto = this.translateProto(proto, direction); 229 | if (!handlerCollection.has(translatedProto)) { 230 | handlerCollection.set(translatedProto, []); 231 | } 232 | handlerCollection.get(translatedProto).push(handlerObj); 233 | } 234 | 235 | async handleBuffer(messageBuffer: Buffer, direction: string, protoFilter?: string[], params?: any, preconnect = false): Promise { 236 | let feedback: Feedback = null; 237 | let messageLength = 0; 238 | let bufferProto = 0; 239 | let datas: Buffer[] = []; 240 | const limit = preconnect ? protoFilter.length * 3 : this.singleHandleLimit; 241 | for (let l = 0; l < limit; ++l) { 242 | if (messageLength === 0) { 243 | if (messageBuffer.length >= 2) { 244 | messageLength = messageBuffer.readUInt16LE(0); 245 | } else { 246 | if (messageBuffer.length !== 0) { 247 | feedback = { 248 | type: "BUFFER_LENGTH", 249 | message: `Bad ${direction} buffer length` 250 | }; 251 | } 252 | break; 253 | } 254 | } else if (bufferProto === 0) { 255 | if (messageBuffer.length >= 3) { 256 | bufferProto = messageBuffer.readUInt8(2); 257 | } else { 258 | feedback = { 259 | type: "PROTO_LENGTH", 260 | message: `Bad ${direction} proto length` 261 | }; 262 | break; 263 | } 264 | } else { 265 | if (messageBuffer.length >= 2 + messageLength) { 266 | const proto = this.constants[direction][bufferProto]; 267 | let cancel: string | boolean | Buffer = proto && protoFilter && !protoFilter.includes(proto); 268 | if (cancel && preconnect) { 269 | feedback = { 270 | type: "INVALID_PACKET", 271 | message: `${direction} proto not allowed` 272 | }; 273 | break; 274 | } 275 | let buffer = messageBuffer.slice(3, 2 + messageLength); 276 | let bufferMutated = false; 277 | //console.log(l, direction, proto, cancel); 278 | for (let priority = 0; priority < 4; ++priority) { 279 | if (cancel) { 280 | break; 281 | } 282 | const handlerCollection: Map = this.handlers[direction][priority]; 283 | if (proto && handlerCollection.has(bufferProto)) { 284 | let struct = this.structs.get(this.proto_structs[direction][proto]); 285 | for (const handler of handlerCollection.get(bufferProto)) { 286 | let info = null; 287 | if (struct) { 288 | struct._setBuff(buffer); 289 | info = _.clone(struct.fields); 290 | } 291 | cancel = await handler.handle(buffer, info, datas, params); 292 | if (cancel) { 293 | if (Buffer.isBuffer(cancel)) { 294 | buffer = cancel as any; 295 | bufferMutated = true; 296 | cancel = false; 297 | } else if (typeof cancel === "string") { 298 | if (cancel === '_cancel') { 299 | return { 300 | datas: [], 301 | feedback 302 | } 303 | } else if (cancel.startsWith('_shrink_')) { 304 | const targetShrinkCount = parseInt(cancel.slice(8)); 305 | if (targetShrinkCount > buffer.length) { 306 | cancel = true; 307 | } else { 308 | buffer = buffer.slice(0, buffer.length - targetShrinkCount); 309 | bufferMutated = true; 310 | cancel = false; 311 | } 312 | } 313 | } 314 | break; 315 | } 316 | } 317 | } 318 | } 319 | if (!cancel) { 320 | if (bufferMutated) { 321 | const newLength = buffer.length + 1; 322 | messageBuffer.writeUInt16LE(newLength, 0); 323 | datas.push(Buffer.concat([messageBuffer.slice(0, 3), buffer])); 324 | } else { 325 | datas.push(messageBuffer.slice(0, 2 + messageLength)); 326 | } 327 | } 328 | messageBuffer = messageBuffer.slice(2 + messageLength); 329 | messageLength = 0; 330 | bufferProto = 0; 331 | } else { 332 | if (direction === "STOC" || messageLength !== 17735) { 333 | feedback = { 334 | type: "MESSAGE_LENGTH", 335 | message: `Bad ${direction} message length` 336 | }; 337 | } 338 | break; 339 | } 340 | } 341 | if (l === limit - 1) { 342 | feedback = { 343 | type: "OVERSIZE", 344 | message: `Oversized ${direction} ${limit}` 345 | }; 346 | } 347 | } 348 | return { 349 | datas, 350 | feedback 351 | }; 352 | } 353 | 354 | } 355 | -------------------------------------------------------------------------------- /ygopro-tournament.js: -------------------------------------------------------------------------------- 1 | /* 2 | ygopro-tournament.js 3 | ygopro tournament util 4 | Author: mercury233 5 | License: MIT 6 | 7 | 不带参数运行时,会建立一个服务器,调用API执行对应操作 8 | */ 9 | const http = require('http'); 10 | const https = require('https'); 11 | const fs = require('fs'); 12 | const url = require('url'); 13 | const request = require('request'); 14 | const formidable = require('formidable'); 15 | const _ = require('underscore'); 16 | _.str = require('underscore.string'); 17 | _.mixin(_.str.exports()); 18 | const loadJSON = require('load-json-file').sync; 19 | const axios = require('axios'); 20 | 21 | const auth = require('./ygopro-auth.js'); 22 | 23 | const settings = loadJSON('./config/config.json'); 24 | config = settings.modules.tournament_mode; 25 | challonge_config = settings.modules.challonge; 26 | const { Challonge } = require('./challonge'); 27 | const challonge = new Challonge(challonge_config); 28 | ssl_config = settings.modules.http.ssl; 29 | 30 | const _async = require("async"); 31 | const os = require("os"); 32 | const PROCESS_COUNT = os.cpus().length; 33 | 34 | //http长连接 35 | let responder; 36 | 37 | config.wallpapers=[""]; 38 | request({ 39 | url: "http://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=8&mkt=zh-CN", 40 | json: true 41 | }, function(error, response, body) { 42 | if (_.isString(body)) { 43 | console.log("wallpapers bad json", body); 44 | } 45 | else if (error || !body) { 46 | console.log('wallpapers error', error, response); 47 | } 48 | else { 49 | config.wallpapers=[]; 50 | for (const i in body.images) { 51 | const wallpaper=body.images[i]; 52 | const img={ 53 | "url": "http://s.cn.bing.net"+wallpaper.urlbase+"_768x1366.jpg", 54 | "desc": wallpaper.copyright 55 | } 56 | config.wallpapers.push(img); 57 | } 58 | } 59 | }); 60 | 61 | //输出反馈信息,如有http长连接则输出到http,否则输出到控制台 62 | const sendResponse = function(text) { 63 | text=""+text; 64 | if (responder) { 65 | text=text.replace(/\n/g,"
"); 66 | responder.write("data: " + text + "\n\n"); 67 | } 68 | else { 69 | console.log(text); 70 | } 71 | } 72 | 73 | //读取指定卡组 74 | const readDeck = async function(deck_name, deck_full_path) { 75 | const deck={}; 76 | deck.name=deck_name; 77 | deck_text = await fs.promises.readFile(deck_full_path, { encoding: "ASCII" }); 78 | deck_array = deck_text.split(/\r?\n/); 79 | deck.main = []; 80 | deck.extra = []; 81 | deck.side = []; 82 | current_deck = deck.main; 83 | for (l in deck_array) { 84 | line = deck_array[l]; 85 | if (line.indexOf("#extra") >= 0) { 86 | current_deck = deck.extra; 87 | } 88 | if (line.indexOf("!side") >= 0) { 89 | current_deck = deck.side; 90 | } 91 | card = parseInt(line); 92 | if (!isNaN(card) && !line.endsWith("#")) { 93 | current_deck.push(card); 94 | } 95 | } 96 | return deck; 97 | } 98 | 99 | //读取指定文件夹中所有卡组 100 | const getDecks = function(callback) { 101 | const decks=[]; 102 | _async.auto({ 103 | readDir: (done) => { 104 | fs.readdir(config.deck_path, done); 105 | }, 106 | handleDecks: ["readDir", (results, done) => { 107 | const decks_list = results.readDir; 108 | _async.each(decks_list, async(deck_name) => { 109 | if (_.endsWith(deck_name, ".ydk")) { 110 | const deck = await readDeck(deck_name, config.deck_path + deck_name); 111 | decks.push(deck); 112 | } 113 | }, done) 114 | }] 115 | }, (err) => { 116 | callback(err, decks); 117 | }); 118 | 119 | } 120 | 121 | const delDeck = function (deck_name, callback) { 122 | if (deck_name.startsWith("../") || deck_name.match(/\/\.\.\//)) { //security issue 123 | callback("Invalid deck"); 124 | } 125 | fs.unlink(config.deck_path + deck_name, callback); 126 | } 127 | 128 | const clearDecks = function (callback) { 129 | _async.auto({ 130 | deckList: (done) => { 131 | fs.readdir(config.deck_path, done); 132 | }, 133 | removeAll: ["deckList", (results, done) => { 134 | const decks_list = results.deckList; 135 | _async.each(decks_list, delDeck, done); 136 | }] 137 | }, callback); 138 | } 139 | 140 | const UploadToChallonge = async function () { 141 | if (!challonge_config.enabled) { 142 | sendResponse("未开启Challonge模式。"); 143 | return false; 144 | } 145 | sendResponse("开始读取玩家列表。"); 146 | const decks_list = fs.readdirSync(config.deck_path); 147 | const player_list = []; 148 | for (const k in decks_list) { 149 | const deck_name = decks_list[k]; 150 | if (_.endsWith(deck_name, ".ydk")) { 151 | player_list.push(deck_name.slice(0, deck_name.length - 4)); 152 | } 153 | } 154 | if (!player_list.length) { 155 | sendResponse("玩家列表为空。"); 156 | return false; 157 | } 158 | sendResponse("读取玩家列表完毕,共有" + player_list.length + "名玩家。"); 159 | try { 160 | sendResponse("开始清空 Challonge 玩家列表。"); 161 | await challonge.clearParticipants(); 162 | sendResponse("开始上传玩家列表至 Challonge。"); 163 | for (const chunk of _.chunk(player_list, 10)) { 164 | sendResponse(`开始上传玩家 ${chunk.join(', ')} 至 Challonge。`); 165 | await challonge.uploadParticipants(chunk); 166 | } 167 | sendResponse("玩家列表上传完成。"); 168 | } catch (e) { 169 | sendResponse("Challonge 上传失败:" + e.message); 170 | } 171 | return true; 172 | } 173 | 174 | const receiveDecks = function(files, callback) { 175 | const result = []; 176 | _async.eachSeries(files, async(file) => { 177 | if (_.endsWith(file.name, ".ydk")) { 178 | const deck = await readDeck(file.name, file.path); 179 | if (deck.main.length >= 40) { 180 | fs.createReadStream(file.path).pipe(fs.createWriteStream(config.deck_path + file.name)); 181 | result.push({ 182 | file: file.name, 183 | status: "OK" 184 | }); 185 | } 186 | else { 187 | result.push({ 188 | file: file.name, 189 | status: "卡组不合格" 190 | }); 191 | } 192 | } 193 | else { 194 | result.push({ 195 | file: file.name, 196 | status: "不是卡组文件" 197 | }); 198 | } 199 | }, (err) => { 200 | callback(err, result); 201 | }); 202 | } 203 | 204 | //建立一个http服务器,接收API操作 205 | async function requestListener(req, res) { 206 | const u = url.parse(req.url, true); 207 | 208 | /*if (u.query.password !== config.password) { 209 | res.writeHead(403); 210 | res.end("Auth Failed."); 211 | return; 212 | }*/ 213 | 214 | if (u.pathname === '/api/upload_decks' && req.method.toLowerCase() == 'post') { 215 | if (!await auth.auth(u.query.username, u.query.password, "deck_dashboard_write", "upload_deck")) { 216 | res.writeHead(403); 217 | res.end("Auth Failed."); 218 | return; 219 | } 220 | const form = new formidable.IncomingForm(); 221 | form.parse(req, function(err, fields, files) { 222 | receiveDecks(files, (err, result) => { 223 | if (err) { 224 | console.error(`Upload error: ${err}`); 225 | res.writeHead(500, { 226 | "Access-Control-Allow-origin": "*", 227 | 'content-type': 'text/plain' 228 | }); 229 | res.end(JSON.stringify({error: err.toString()})); 230 | return; 231 | } 232 | res.writeHead(200, { 233 | "Access-Control-Allow-origin": "*", 234 | 'content-type': 'text/plain' 235 | }); 236 | res.end(JSON.stringify(result)); 237 | }); 238 | }); 239 | } 240 | else if (u.pathname === '/api/msg') { 241 | if (!await auth.auth(u.query.username, u.query.password, "deck_dashboard_read", "login_deck_dashboard")) { 242 | res.writeHead(403); 243 | res.end("Auth Failed."); 244 | return; 245 | } 246 | res.writeHead(200, { 247 | "Access-Control-Allow-origin": "*", 248 | "Content-Type": "text/event-stream", 249 | "Cache-Control": "no-cache", 250 | "Connection": "keep-alive" 251 | }); 252 | 253 | res.on("close", function(){ 254 | responder = null; 255 | }); 256 | 257 | responder = res; 258 | 259 | sendResponse("已连接。"); 260 | } 261 | else if (u.pathname === '/api/get_bg') { 262 | if (!await auth.auth(u.query.username, u.query.password, "deck_dashboard_read", "login_deck_dashboard")) { 263 | res.writeHead(403); 264 | res.end("Auth Failed."); 265 | return; 266 | } 267 | res.writeHead(200); 268 | res.end(u.query.callback+'('+JSON.stringify(config.wallpapers[Math.floor(Math.random() * config.wallpapers.length)])+');'); 269 | } 270 | else if (u.pathname === '/api/get_decks') { 271 | if (!await auth.auth(u.query.username, u.query.password, "deck_dashboard_read", "get_decks")) { 272 | res.writeHead(403); 273 | res.end("Auth Failed."); 274 | return; 275 | } 276 | getDecks((err, decks) => { 277 | if (err) { 278 | res.writeHead(500); 279 | res.end(u.query.callback + '(' + err.toString() +');'); 280 | } else { 281 | res.writeHead(200); 282 | res.end(u.query.callback+'('+JSON.stringify(decks)+');'); 283 | } 284 | }) 285 | } 286 | else if (u.pathname === '/api/del_deck') { 287 | if (!await auth.auth(u.query.username, u.query.password, "deck_dashboard_write", "delete_deck")) { 288 | res.writeHead(403); 289 | res.end("Auth Failed."); 290 | return; 291 | } 292 | res.writeHead(200); 293 | delDeck(u.query.msg, (err) => { 294 | let result; 295 | if (err) { 296 | result = "删除卡组 " + u.query.msg + "失败: " + err.toString(); 297 | } else { 298 | result = "删除卡组 " + u.query.msg + "成功。"; 299 | } 300 | res.writeHead(200); 301 | res.end(u.query.callback+'("'+result+'");'); 302 | }); 303 | } 304 | else if (u.pathname === '/api/clear_decks') { 305 | if (!await auth.auth(u.query.username, u.query.password, "deck_dashboard_write", "clear_decks")) { 306 | res.writeHead(403); 307 | res.end("Auth Failed."); 308 | return; 309 | } 310 | clearDecks((err) => { 311 | let result; 312 | if (err) { 313 | result = "删除全部卡组失败。" + err.toString(); 314 | } else { 315 | result = "删除全部卡组成功。"; 316 | } 317 | res.writeHead(200); 318 | res.end(u.query.callback+'("'+result+'");'); 319 | }); 320 | } 321 | else if (u.pathname === '/api/upload_to_challonge') { 322 | if (!await auth.auth(u.query.username, u.query.password, "deck_dashboard_write", "upload_to_challonge")) { 323 | res.writeHead(403); 324 | res.end("Auth Failed."); 325 | return; 326 | } 327 | res.writeHead(200); 328 | const result = await UploadToChallonge(); 329 | res.end(u.query.callback+'("操作完成。");'); 330 | } 331 | else { 332 | res.writeHead(400); 333 | res.end("400"); 334 | } 335 | 336 | } 337 | 338 | if (ssl_config.enabled) { 339 | const ssl_cert = fs.readFileSync(ssl_config.cert); 340 | const ssl_key = fs.readFileSync(ssl_config.key); 341 | const options = { 342 | cert: ssl_cert, 343 | key: ssl_key 344 | } 345 | https.createServer(options, requestListener).listen(config.port); 346 | } else { 347 | http.createServer(requestListener).listen(config.port); 348 | } 349 | -------------------------------------------------------------------------------- /Replay.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as lzma from 'lzma'; 3 | 4 | /** A minimal deck representation */ 5 | export interface DeckObject { 6 | main: number[]; 7 | ex: number[]; 8 | } 9 | 10 | export const SEED_COUNT = 8; 11 | export const REPLAY_ID_YRP1 = 0x31707279; 12 | export const REPLAY_ID_YRP2 = 0x32707279; 13 | 14 | /** 15 | * Metadata stored at the beginning of every replay file. 16 | */ 17 | export class ReplayHeader { 18 | static readonly REPLAY_COMPRESSED_FLAG = 0x1; 19 | static readonly REPLAY_TAG_FLAG = 0x2; 20 | static readonly REPLAY_DECODED_FLAG = 0x4; 21 | static readonly REPLAY_SINGLE_MODE = 0x8; 22 | static readonly REPLAY_UNIFORM = 0x10; 23 | 24 | id = 0; 25 | version = 0; 26 | flag = 0; 27 | seed = 0; 28 | dataSizeRaw: number[] = []; 29 | hash = 0; 30 | props: number[] = []; 31 | seedSequence: number[] = []; 32 | headerVersion = 0; 33 | value1 = 0; 34 | value2 = 0; 35 | value3 = 0; 36 | 37 | 38 | /** Decompressed size as little‑endian 32‑bit */ 39 | get dataSize(): number { 40 | return Buffer.from(this.dataSizeRaw).readUInt32LE(0); 41 | } 42 | 43 | get isTag(): boolean { 44 | return (this.flag & ReplayHeader.REPLAY_TAG_FLAG) !== 0; 45 | } 46 | 47 | get isCompressed(): boolean { 48 | return (this.flag & ReplayHeader.REPLAY_COMPRESSED_FLAG) !== 0; 49 | } 50 | 51 | /** Compose a valid 13‑byte LZMA header for this replay */ 52 | getLzmaHeader(): Buffer { 53 | const bytes = [ 54 | ...this.props.slice(0, 5), 55 | ...this.dataSizeRaw, 56 | 0, 57 | 0, 58 | 0, 59 | 0, 60 | ]; 61 | return Buffer.from(bytes); 62 | } 63 | } 64 | 65 | /** Utility for reading little‑endian primitives from a Buffer */ 66 | class ReplayReader { 67 | private pointer = 0; 68 | constructor(private readonly buffer: Buffer) {} 69 | 70 | private advance(size: number, read: () => T): T { 71 | const value = read(); 72 | this.pointer += size; 73 | return value; 74 | } 75 | 76 | readByte(): number { 77 | return this.advance(1, () => this.buffer.readUInt8(this.pointer)); 78 | } 79 | 80 | readByteArray(length: number): number[] { 81 | const out: number[] = []; 82 | for (let i = 0; i < length; i++) out.push(this.readByte()); 83 | return out; 84 | } 85 | 86 | readInt8(): number { 87 | return this.advance(1, () => this.buffer.readInt8(this.pointer)); 88 | } 89 | 90 | readUInt8(): number { 91 | return this.advance(1, () => this.buffer.readUInt8(this.pointer)); 92 | } 93 | 94 | readInt16(): number { 95 | return this.advance(2, () => this.buffer.readInt16LE(this.pointer)); 96 | } 97 | 98 | readInt32(): number { 99 | return this.advance(4, () => this.buffer.readInt32LE(this.pointer)); 100 | } 101 | 102 | readUInt16(): number { 103 | return this.advance(2, () => this.buffer.readUInt16LE(this.pointer)); 104 | } 105 | 106 | readUInt32(): number { 107 | return this.advance(4, () => this.buffer.readUInt32LE(this.pointer)); 108 | } 109 | 110 | readAll(): Buffer { 111 | return this.buffer.slice(this.pointer); 112 | } 113 | 114 | readString(length: number): string | null { 115 | if (this.pointer + length > this.buffer.length) return null; 116 | const raw = this.buffer 117 | .slice(this.pointer, this.pointer + length) 118 | .toString('utf16le'); 119 | this.pointer += length; 120 | return raw.split('\u0000')[0]; 121 | } 122 | 123 | readRaw(length: number): Buffer | null { 124 | if (this.pointer + length > this.buffer.length) return null; 125 | const buf = this.buffer.slice(this.pointer, this.pointer + length); 126 | this.pointer += length; 127 | return buf; 128 | } 129 | } 130 | 131 | /** Utility for writing little‑endian primitives into a Buffer */ 132 | class ReplayWriter { 133 | private pointer = 0; 134 | constructor(public buffer: Buffer) {} 135 | 136 | private advance(action: () => void, size: number): void { 137 | action(); 138 | this.pointer += size; 139 | } 140 | 141 | writeByte(val: number): void { 142 | this.advance(() => this.buffer.writeUInt8(val, this.pointer), 1); 143 | } 144 | 145 | writeByteArray(values: Iterable): void { 146 | for (const v of values) this.writeByte(v); 147 | } 148 | 149 | writeInt8(val: number): void { 150 | this.advance(() => this.buffer.writeInt8(val, this.pointer), 1); 151 | } 152 | 153 | writeUInt8(val: number): void { 154 | this.advance(() => this.buffer.writeUInt8(val, this.pointer), 1); 155 | } 156 | 157 | writeInt16(val: number): void { 158 | this.advance(() => this.buffer.writeInt16LE(val, this.pointer), 2); 159 | } 160 | 161 | writeInt32(val: number): void { 162 | this.advance(() => this.buffer.writeInt32LE(val, this.pointer), 4); 163 | } 164 | 165 | writeUInt16(val: number): void { 166 | this.advance(() => this.buffer.writeUInt16LE(val, this.pointer), 2); 167 | } 168 | 169 | writeUInt32(val: number): void { 170 | this.advance(() => this.buffer.writeUInt32LE(val, this.pointer), 4); 171 | } 172 | 173 | writeAll(buf: Buffer): void { 174 | this.buffer = Buffer.concat([this.buffer, buf]); 175 | } 176 | 177 | writeString(val: string | null, length?: number): void { 178 | const raw = Buffer.from(val ?? '', 'utf16le'); 179 | const bytes = [...raw]; 180 | if (length !== undefined) { 181 | const padding = new Array(Math.max(length - bytes.length, 0)).fill(0); 182 | this.writeByteArray([...bytes, ...padding]); 183 | } else { 184 | this.writeByteArray(bytes); 185 | } 186 | } 187 | } 188 | 189 | export class Replay { 190 | header: ReplayHeader | null = null; 191 | hostName = ''; 192 | clientName = ''; 193 | startLp = 0; 194 | startHand = 0; 195 | drawCount = 0; 196 | opt = 0; 197 | 198 | hostDeck: DeckObject | null = null; 199 | clientDeck: DeckObject | null = null; 200 | 201 | tagHostName: string | null = null; 202 | tagClientName: string | null = null; 203 | tagHostDeck: DeckObject | null = null; 204 | tagClientDeck: DeckObject | null = null; 205 | 206 | responses: Buffer[] = []; 207 | 208 | /** All deck objects in play order */ 209 | get decks(): DeckObject[] { 210 | return this.isTag 211 | ? [ 212 | this.hostDeck!, 213 | this.clientDeck!, 214 | this.tagHostDeck!, 215 | this.tagClientDeck!, 216 | ] 217 | : [this.hostDeck!, this.clientDeck!]; 218 | } 219 | 220 | get isTag(): boolean { 221 | return this.header?.isTag ?? false; 222 | } 223 | 224 | /* ------------------ Static helpers ------------------ */ 225 | 226 | static async fromFile(path: string): Promise { 227 | return Replay.fromBuffer(await fs.promises.readFile(path)); 228 | } 229 | 230 | static fromBuffer(buffer: Buffer): Replay { 231 | const headerReader = new ReplayReader(buffer); 232 | const header = Replay.readHeader(headerReader); 233 | const raw = headerReader.readAll(); 234 | 235 | const body = header.isCompressed 236 | ? Replay.decompressBody(header, raw) 237 | : raw; 238 | 239 | const bodyReader = new ReplayReader(body); 240 | return Replay.readReplay(header, bodyReader); 241 | } 242 | 243 | private static decompressBody(header: ReplayHeader, raw: Buffer): Buffer { 244 | const lzmaBuffer = Buffer.concat([header.getLzmaHeader(), raw]); 245 | // lzma‑native provides synchronous helpers. 246 | return Buffer.from(lzma.decompress(lzmaBuffer)); 247 | } 248 | 249 | private static readHeader(reader: ReplayReader): ReplayHeader { 250 | const h = new ReplayHeader(); 251 | h.id = reader.readUInt32(); 252 | h.version = reader.readUInt32(); 253 | h.flag = reader.readUInt32(); 254 | h.seed = reader.readUInt32(); 255 | h.dataSizeRaw = reader.readByteArray(4); 256 | h.hash = reader.readUInt32(); 257 | h.props = reader.readByteArray(8); 258 | if (h.id === REPLAY_ID_YRP2) { 259 | for(let i = 0; i < SEED_COUNT; i++) { 260 | h.seedSequence.push(reader.readUInt32()); 261 | } 262 | h.headerVersion = reader.readUInt32(); 263 | h.value1 = reader.readUInt32(); 264 | h.value2 = reader.readUInt32(); 265 | h.value3 = reader.readUInt32(); 266 | } 267 | return h; 268 | } 269 | 270 | private static readReplay(header: ReplayHeader, reader: ReplayReader): Replay { 271 | const r = new Replay(); 272 | r.header = header; 273 | 274 | r.hostName = reader.readString(40) ?? ''; 275 | if (header.isTag) { 276 | r.tagHostName = reader.readString(40); 277 | r.tagClientName = reader.readString(40); 278 | } 279 | r.clientName = reader.readString(40) ?? ''; 280 | 281 | r.startLp = reader.readInt32(); 282 | r.startHand = reader.readInt32(); 283 | r.drawCount = reader.readInt32(); 284 | r.opt = reader.readInt32(); 285 | 286 | r.hostDeck = Replay.readDeck(reader); 287 | if (header.isTag) { 288 | r.tagHostDeck = Replay.readDeck(reader); 289 | r.tagClientDeck = Replay.readDeck(reader); 290 | } 291 | r.clientDeck = Replay.readDeck(reader); 292 | 293 | r.responses = Replay.readResponses(reader); 294 | return r; 295 | } 296 | 297 | /* ------------------ Deck helpers ------------------ */ 298 | 299 | private static readDeck(reader: ReplayReader): DeckObject { 300 | return { 301 | main: Replay.readDeckPack(reader), 302 | ex: Replay.readDeckPack(reader), 303 | }; 304 | } 305 | 306 | private static readDeckPack(reader: ReplayReader): number[] { 307 | const length = reader.readInt32(); 308 | const cards: number[] = []; 309 | for (let i = 0; i < length; i++) cards.push(reader.readInt32()); 310 | return cards; 311 | } 312 | 313 | /* ------------------ Response helpers ------------------ */ 314 | 315 | private static readResponses(reader: ReplayReader): Buffer[] { 316 | const out: Buffer[] = []; 317 | while (true) { 318 | try { 319 | let length = reader.readUInt8(); 320 | if (length > 64) length = 64; 321 | const segment = reader.readRaw(length); 322 | if (!segment) break; 323 | out.push(segment); 324 | } catch { 325 | break; 326 | } 327 | } 328 | return out; 329 | } 330 | 331 | /* ------------------ Writing ------------------ */ 332 | 333 | toBuffer(): Buffer { 334 | if (!this.header) throw new Error('Header not initialised'); 335 | 336 | const headerWriter = new ReplayWriter(Buffer.alloc(32)); 337 | this.writeHeader(headerWriter); 338 | 339 | const deckSize = (d: DeckObject | null) => 340 | ((d?.main.length ?? 0) + (d?.ex.length ?? 0)) * 4 + 8; 341 | 342 | const responseSize = this.responses.reduce((s, b) => s + b.length + 1, 0); 343 | 344 | let contentSize = 345 | 96 + deckSize(this.hostDeck) + deckSize(this.clientDeck) + responseSize; 346 | 347 | if (this.header.isTag) { 348 | contentSize += 349 | deckSize(this.tagHostDeck) + deckSize(this.tagClientDeck) + 80; 350 | } 351 | 352 | const contentWriter = new ReplayWriter(Buffer.alloc(contentSize)); 353 | this.writeContent(contentWriter); 354 | 355 | let body = contentWriter.buffer; 356 | if (this.header.isCompressed) { 357 | body = Buffer.from(lzma.compress(body)); 358 | body = body.slice(13); // strip header like original implementation 359 | } 360 | 361 | return Buffer.concat([headerWriter.buffer, body]); 362 | } 363 | 364 | async writeToFile(path: string): Promise { 365 | await fs.promises.writeFile(path, this.toBuffer()); 366 | } 367 | 368 | private writeHeader(w: ReplayWriter): void { 369 | w.writeUInt32(this.header!.id); 370 | w.writeUInt32(this.header!.version); 371 | w.writeUInt32(this.header!.flag); 372 | w.writeUInt32(this.header!.seed); 373 | w.writeByteArray(this.header!.dataSizeRaw); 374 | w.writeUInt32(this.header!.hash); 375 | w.writeByteArray(this.header!.props); 376 | if (this.header!.id === REPLAY_ID_YRP2) { 377 | for (let i = 0; i < SEED_COUNT; i++) { 378 | w.writeUInt32(this.header!.seedSequence[i]); 379 | } 380 | w.writeUInt32(this.header!.headerVersion); 381 | w.writeUInt32(this.header!.value1); 382 | w.writeUInt32(this.header!.value2); 383 | w.writeUInt32(this.header!.value3); 384 | } 385 | } 386 | 387 | private writeContent(w: ReplayWriter): void { 388 | w.writeString(this.hostName, 40); 389 | if (this.header!.isTag) { 390 | w.writeString(this.tagHostName, 40); 391 | w.writeString(this.tagClientName, 40); 392 | } 393 | w.writeString(this.clientName, 40); 394 | 395 | w.writeInt32(this.startLp); 396 | w.writeInt32(this.startHand); 397 | w.writeInt32(this.drawCount); 398 | w.writeInt32(this.opt); 399 | 400 | Replay.writeDeck(w, this.hostDeck); 401 | if (this.header!.isTag) { 402 | Replay.writeDeck(w, this.tagHostDeck); 403 | Replay.writeDeck(w, this.tagClientDeck); 404 | } 405 | Replay.writeDeck(w, this.clientDeck); 406 | 407 | Replay.writeResponses(w, this.responses); 408 | } 409 | 410 | private static writeDeck(w: ReplayWriter, d: DeckObject | null): void { 411 | if (!d) { 412 | w.writeInt32(0); 413 | w.writeInt32(0); 414 | return; 415 | } 416 | Replay.writeDeckPack(w, d.main); 417 | Replay.writeDeckPack(w, d.ex); 418 | } 419 | 420 | private static writeDeckPack(w: ReplayWriter, pack: number[]): void { 421 | w.writeInt32(pack.length); 422 | for (const card of pack) w.writeInt32(card); 423 | } 424 | 425 | private static writeResponses(w: ReplayWriter, res: Buffer[]): void { 426 | for (const buf of res) { 427 | w.writeUInt8(buf.length); 428 | w.writeByteArray(buf); 429 | } 430 | } 431 | } 432 | --------------------------------------------------------------------------------