Commit bc918cb0 authored by 神楽坂玲奈's avatar 神楽坂玲奈

init

parents
/.idea/
*.js
*.js.map
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
{
"port": 17911,
"version": 4924,
"hostinfo": {
"lflist": 0,
"rule": 0,
"mode": 0,
"comment": "rule: 0=OCGONLY, 1=TCGONLY, 2=OT; mode: 0=SINGLE, 1=MATCH, 2=TAG",
"enable_priority": false,
"no_check_deck": false,
"no_shuffle_deck": false,
"start_lp": 8000,
"start_hand": 5,
"draw_count": 1,
"time_limit": 180
},
"modules": {
"welcome": "MyCard YGOPro Server",
"update": "请更新游戏版本",
"stop": false,
"tips": {
"enabled": true,
"get": false
},
"dialogues": {
"enabled": true,
"get": "http://mercury233.me/ygosrv233/dialogues.json"
},
"random_duel": {
"enabled": false,
"hang_timeout": 90
},
"cloud_replay": {
"enabled": false,
"redis_port": 6379,
"enable_halfway_watch": true
},
"windbot": {
"enabled": false,
"botlist": "./windbot/bots.json",
"spawn": false,
"port": 12399
},
"mycard": {
"enabled": false,
"auth_base_url": "https://ygobbs.com",
"auth_database": "postgres://233@233.mycard.moe/233",
"auth_key": "233333"
},
"deck_log": {
"enabled": false,
"accesskey": "233",
"local": "./deck_log/",
"post": "https://mycard.moe/ygopro/analytics/deck/text"
},
"arena_mode": {
"enabled": false,
"mode": "entertain",
"comment": "mode: athletic / entertain",
"accesskey": "233",
"post_score": false,
"get_score": false
},
"tournament_mode": {
"enabled": false,
"deck_path": "./decks/",
"duel_log": [],
"password": "123456",
"port": 7933
},
"pre_util": {
"enabled": false,
"port": 7944,
"password": "123456",
"git_html_path": "../mercury233.github.io/",
"html_path": "../mercury233.github.io/ygosrv233/",
"html_filename": "pre.html",
"git_db_path": "../ygopro-pre-data/",
"db_path": "../ygopro-pre-data/unofficial/",
"html_img_rel_path": "pre/pics/",
"html_img_thumbnail": "thumbnail/",
"ygopro_path": "../ygopro-pre/",
"only_show_dbs": {
"news.cdb": true,
"pre-release.cdb": true
},
"html_gits": [
{
"name": "GitHub",
"push": ["push", "origin"]
},
{
"name": "Coding",
"push": ["push", "coding", "master:master"]
}
]
},
"http": {
"port": 17922,
"password": "123456",
"websocket_roomlist": true,
"public_roomlist": false,
"ssl": {
"enabled": false,
"port": 17923,
"cert": "ssl/fullchain.pem",
"key": "ssl/privkey.pem"
}
}
},
"ban": {
"banned_user": [],
"banned_ip": [],
"badword_level0": ["滚", "衮", "操", "草", "艹", "狗", "日", "曰", "妈", "娘", "逼"],
"badword_level1": ["傻逼", "鸡巴"],
"badword_level2": ["死妈", "草你妈"],
"badword_level3": ["迷奸", "仿真枪"],
"illegal_id": ["^Lv\\.-*\\d+\\s*(.*)", "^VIP\\.\\d+\\s*(.*)"],
"spam_word": ["——"]
},
"tips": [
"欢迎来到本服务器",
"本服务器使用萌卡代码搭建"
],
"dialogues": {
"46986414": [
"出来吧,我最强的仆人,黑魔导!"
],
"58481572": [
"我们来做朋友吧!"
]
}
}
\ No newline at end of file
{}
\ No newline at end of file
{
"name": "ygopro-server",
"version": "3.0.0",
"description": "a server for ygopro",
"repository": "github:mycard/ygopro-server",
"keywords": [
"mycard",
"ygopro",
"server"
],
"author": "zh99998 <zh99998@gmail.com>, mercury233 <me@mercury233.me>",
"dependencies": {
"i18n": "^0.8.3",
"node-fetch": "^1.6.3",
"ref-array": "^1.2.0",
"ref-struct": "^1.1.0",
"ws": "^1.1.1"
},
"license": "AGPL-3.0",
"devDependencies": {
"@types/i18n": "^0.5.20",
"@types/node": "^6.0.52",
"@types/node-fetch": "^1.6.6",
"@types/ref-array": "0.0.28",
"@types/ref-struct": "0.0.28",
"@types/ws": "0.0.37",
"typescript": "^2.1.4"
}
}
/**
* Created by zh99998 on 2016/12/24.
*/
import {Transform} from "stream";
import {ERROR_MSG, STOC_CHAT, Struct, Array, COLORS, ERRMSG} from "./types";
import ArrayType = require("ref-array");
import assert = require('assert');
import StructType = require("ref-struct");
export class Protocol extends Transform {
buffer = new Buffer(0);
size = 0;
follows = new Map<StructType, Function[]>();
types: Map<number, StructType>;
types_reverse: Map<StructType, number>;
constructor(types: Map<number, StructType>) {
super();
this.types = types;
this.types_reverse = new Map(<[StructType, number][]>Array.from(types).map(([key, value]) => [value, key]));
}
follow(proto, callback) {
let array = this.follows.get(proto);
if (!array) {
array = [];
this.follows.set(proto, array);
}
array.push(callback);
}
send(data: Struct) {
let id = this.types_reverse.get(<StructType>data.constructor);
if (!id) {
throw 'send unknown proto'
}
let buffer = data['ref.buffer'];
let length_buffer = new Buffer(2);
length_buffer.writeUInt16LE(buffer.length + 1, 0);
let id_buffer = Buffer.from([id]);
this.push(length_buffer);
this.push(id_buffer);
this.push(buffer);
}
send_chat(msg: string, player: number = COLORS.LIGHTBLUE) {
for (let line of msg.split("\n")) {
if (player >= 10) {
line = "[System]: " + line
}
let type = <ArrayType<number>>STOC_CHAT.fields['msg'].type;
let buffer = Buffer.alloc(type.size);
Buffer.from(msg, 'utf16le').copy(buffer);
let data = new STOC_CHAT({player: player, msg: new type(buffer)});
this.send(data);
}
}
send_die(msg: string) {
this.send_chat(msg, COLORS.RED);
this.send(new ERROR_MSG({msg: ERRMSG.JOINERROR, code: 2}));
this.end()
}
async _transform(chunk, encoding, callback) {
// 这个方法会在收到数据的时候被调用,类似于on data
// 收到数据后,先跟之前未处理的数据拼成一整个buffer,然后取当前需要的数据长度,先固定收包头长度2,然后再收包头指示的长度
assert(encoding == 'buffer');
this.buffer = Buffer.concat([this.buffer, chunk]);
while (this.buffer.length >= (this.size || 2)) {
if (this.size == 0) {
// 收到包头,取前2位作为size,其余扔回this.buffer
this.size = this.buffer.readUInt16LE(0);
this.buffer = this.buffer.slice(2);
} else {
// 收到内容,取前size长度作为data,其余扔回this.buffer
let data = this.buffer.slice(0, this.size);
this.buffer = this.buffer.slice(this.size);
// 内容第1位是类型
let type_id = data.readUInt8(0);
let type: StructType = this.types.get(type_id);
if (type) {
// 找到了,解析丫
let follows = this.follows.get(type);
if (follows) {
let message = new type(data.slice(1));
for (let follow of follows) {
await follow(message)
}
}
} else {
// 不认识这个消息
console.warn(`unknown protocol ${type_id}`)
}
// 处理完毕,构造出长度那个包头一起发送。
// 这里要处理完整条消息再一起发,而不要处理完包头就直接发包头,这样可以保证任何时候执行send,对端收到的消息总是完好的,不会在包中间被插入。
let length_buffer = Buffer.alloc(2);
length_buffer.writeUInt16LE(this.size, 0);
this.push(length_buffer); // this.push 是 ReadableStream 的方法,把数据放进去。
this.push(data);
this.size = 0;
}
}
// transform 的 callback,用来表示已经处理完了
callback();
}
// 这个Array<numer> 是指Ref里的Array,不是js的Array。
static readUnicodeString(array: Array<number>) {
let arr = Uint16Array.from(array);
let index = arr.indexOf(0);
if (index != -1) {
arr = arr.slice(0, index);
}
return Buffer.from(arr.buffer).toString('utf16le');
}
}
\ No newline at end of file
import child_process = require("child_process");
import {ChildProcess} from "child_process";
import {Socket} from "net";
import {HostInfo} from "./types";
import {EventEmitter} from "events";
import fetch from "node-fetch";
import net = require("net");
import url = require("url");
const config = require("./config.json");
const windbots: Windbot[] = require(config.modules.windbot.botlist).windbots;
interface Player {
client: Socket,
server: Socket,
username: string,
pass: string,
pos: number
}
interface Windbot {
name: string,
deck: string,
dialog: string
}
export class Room {
started = false;
players: Player[] = [];
constructor(public id: string, public title: string, public hostinfo: HostInfo, public _private: Boolean, public child: ChildProcess, public port: number) {
// private 是关键字,于是换了个名
Room.emitter.emit('create', this);
Room.all.push(this);
new Promise((resolve, reject) => {
this.child.on('exit', resolve);
}).then(() => {
this.destroy();
});
}
static all: Room[] = [];
static emitter = new EventEmitter();
static async join(client: Socket, username: string, pass: string): Promise<Socket> {
let room: Room;
if (pass.length == 0) {
// 随机对战?
}
if (config.modules.windbot && (pass == 'AI' || pass.startsWith('AI#'))) {
// AI
let bots: Windbot[];
if (pass == 'AI') {
bots = windbots;
} else {
let ai = pass.slice(3);
bots = windbots.filter((bot) => bot.name == ai || bot.deck == ai);
if (bots.length == 0) {
throw "未找到该AI角色或卡组"
}
}
let bot = bots[Math.floor(Math.random() * bots.length)];
let hostinfo: HostInfo = Object.assign({}, config.hostinfo, {mode: 0});
room = await Room.create(pass, "AI", hostinfo, false);
await fetch(url.format({
protocol: 'http',
hostname: '127.0.0.1',
port: config.modules.windbot.port,
query: {
name: bot.name,
deck: bot.deck,
dialog: bot.dialog,
host: '127.0.0.1',
port: room.port,
version: config.version,
// password=#{encodeURIComponent(@name)}
// 为什么要让AI也走 ygopro-server? 直接加进房间端口会有问题么
}
}));
}
// 直连加房
if (!room) {
room = this.all.find((room) => room.id == pass);
}
// 直连建房
if (!room) {
let hostinfo: HostInfo = Object.assign({}, config.hostinfo);
if (pass.startsWith('M#')) {
hostinfo.mode = 1;
} else if (pass.startsWith('T#')) {
hostinfo.mode = 2;
}
room = await Room.create(pass, pass.slice(2), hostinfo, false);
}
return room.connect(client, username, pass);
}
static async create(id: string, title: string, hostinfo: HostInfo, _private: boolean): Promise<Room> {
let param = [
'0', // port
hostinfo.lflist.toString(),
hostinfo.rule.toString(),
hostinfo.mode.toString(),
hostinfo.enable_priority ? 'T' : 'F',
hostinfo.no_check_deck ? 'T' : 'F',
hostinfo.no_shuffle_deck ? 'T' : 'F',
hostinfo.start_lp.toString(),
hostinfo.start_hand.toString(),
hostinfo.draw_count.toString(),
hostinfo.time_limit.toString(),
'0' // replay_mode
];
let child = child_process.spawn('./ygopro', param, {cwd: 'ygopro', stdio: ['ignore', 'pipe', 'ignore']});
child.stdout.setEncoding('utf8');
let port = await new Promise<number>((resolve, reject) => {
child.stdout.on('data', (chunk: string) => {
let result = parseInt(chunk);
if (result) {
resolve(result);
} else {
reject(chunk);
}
});
child.on('close', reject);
child.on('error', reject);
child.on('exit', reject);
// Promise 只承认第一次状态转移
});
return new Room(id, '', hostinfo, false, child, port);
}
auth() {
// mycard_auth
}
async connect(client: Socket, username: string, pass: string): Promise<Socket> {
let server = net.connect(this.port);
await new Promise((resolve, reject) => {
server.on('connect', resolve);
server.on('error', reject);
});
this.players.push({
client: client,
server: server,
username: username,
pass: pass,
pos: 0
});
Room.emitter.emit('update', this);
return server
}
destroy() {
let index = Room.all.indexOf(this);
if (index != -1) {
Room.all.splice(index, 1);
}
Room.emitter.emit('destroy', this.id);
}
start() {
this.started = true;
Room.emitter.emit('start', this.id);
}
}
//调试用
Room.emitter.on('create', (room) => {
console.log('room_create', room.id)
});
Room.emitter.on('update', (room) => {
console.log('room_update', room.id)
});
Room.emitter.on('start', (room) => {
console.log('room_start', room.id)
});
Room.emitter.on('destroy', (room_id) => {
console.log('room_destroy', room_id)
});
\ No newline at end of file
/**
* Created by zh99998 on 2016/12/27.
*/
import {Room} from "./room";
import {Server} from "ws";
import WebSocket = require("ws");
const config = require('./config.json');
export class RoomList {
static init(http_server) {
// const http_server = https.createServer({
// key: config.http.ssl.key,
// cert: config.http.ssl.cert,
// });
const websocket_server = new Server({server: http_server});
websocket_server.on("connection", (client: WebSocket) => {
client.send(JSON.stringify({
event: 'init',
data: Room.all.filter((room) => !room._private && !room.started).map(this.room_data)
}))
});
websocket_server.on('error', (error) => {
console.error(error);
});
Room.emitter.on('create', (room) => {
this.broadcast(websocket_server, 'create', this.room_data(room))
});
Room.emitter.on('update', (room) => {
this.broadcast(websocket_server, 'update', this.room_data(room))
});
Room.emitter.on('start', (room) => {
this.broadcast(websocket_server, 'delete', room.id)
});
}
static broadcast(websocket_server, event, data) {
let message = JSON.stringify({
event: event,
data: data
});
for (let client of websocket_server.clients) {
// 不需要 try,如果失败,会在 websocket_server 上触发 error 事件。
client.send(message);
}
}
static room_data(room: Room) {
let result = {
id: room.id,
title: room.title,
users: room.players.map((player) => {
return {username: player.username, position: player.pos}
}),
options: room.hostinfo
}
}
}
import net = require("net");
import {JOIN_GAME, PLAYER_INFO, ERROR_MSG, ERRMSG, COLORS, CTOS, STOC} from "./types";
import {Protocol} from "./protocol";
import {Room} from "./room";
import {RoomList} from "./roomlist";
import i18n = require("i18n");
import child_process = require("child_process");
import https = require("https");
import fs = require("fs");
const config = require('./config.json');
i18n.configure({
locales: ['zh-CN', 'en'],
directory: 'locales'
});
const server = net.createServer((client) => {
let ctos = new Protocol(CTOS);
let stoc = new Protocol(STOC);
client.pipe(ctos);
stoc.pipe(client);
// 每个连入用户一份,生存周期为会话的变量声明在这里
let username, pass;
ctos.follow(PLAYER_INFO, async(data: PLAYER_INFO) => {
username = Protocol.readUnicodeString(data.name);
});
ctos.follow(JOIN_GAME, async(data: JOIN_GAME) => {
if (data.version != config.version) {
stoc.send_chat(i18n.__('invalid_version'), COLORS.RED);
stoc.send(new ERROR_MSG({msg: ERRMSG.VERERROR, code: config.version}));
return client.end();
}
pass = Protocol.readUnicodeString(data.pass);
try {
let server = await Room.join(client, username, pass);
ctos.pipe(server);
server.pipe(stoc);
} catch (error) {
console.error(error);
return stoc.send_die(error.message || error.toString());
}
});
});
server.listen(config.port);
if (config.modules.windbot) {
let windbot = child_process.spawn('mono', ['WindBot.exe', config.modules.windbot.port], {
cwd: 'windbot',
stdio: 'inherit'
});
// config.modules.windbots = require(config.modules.windbot.botlist).windbots
process.on('exit', (code) => {
windbot.kill()
});
}
if (config.modules.http) {
const http_server = https.createServer({
key: fs.readFileSync(config.modules.http.ssl.key),
cert: fs.readFileSync(config.modules.http.ssl.cert),
});
http_server.listen(config.modules.http.port);
if (config.websocket_roomlist) {
RoomList.init(http_server);
}
}
process.on('unhandledRejection', (reason, p) => {
console.error('Unhandled Rejection at: Promise', p, 'reason:', reason);
// application specific logging, throwing an error, or other logic here
});
process.on('exit', (code) => {
for (let room of Room.all) {
room.child.kill()
}
});
\ No newline at end of file
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"sourceMap": true,
"lib":["es2017"],
"skipLibCheck": true
}
}
\ No newline at end of file
/**
* Created by zh99998 on 2016/12/23.
*/
// export declare var types: {
// void: Type; int64: Type; ushort: Type;
// int: Type; uint64: Type; float: Type;
// uint: Type; long: Type; double: Type;
// int8: Type; ulong: Type; Object: Type;
// uint8: Type; longlong: Type; CString: Type;
// int16: Type; ulonglong: Type; bool: Type;
// uint16: Type; char: Type; byte: Type;
// int32: Type; uchar: Type; size_t: Type;
// uint32: Type; short: Type;
// };
import {types} from "ref";
import StructType = require('ref-struct');
import ArrayType = require('ref-array');
// 这玩意儿名字跟内置的Array重了,需要用数组的地方,类型写 number[], 而不要写Array<number>。如果因为这个引发了问题,可以把这个Array改个名,例如改叫ArrayRef
export interface Array<T> {
[i: number]: T; length: number; toArray(): T[];
toJSON(): T[]; inspect(): string; buffer: Buffer; ref(): Buffer;
}
export const HostInfo = StructType({
lflist: types.uint,
rule: types.uchar,
mode: types.uchar,
enable_priority: types.bool,
no_check_deck: types.bool,
no_shuffle_deck: types.bool,
start_lp: types.uint,
start_hand: types.uchar,
draw_count: types.uchar,
time_limit: types.ushort,
});
export interface HostInfo {
lflist: number
rule: number
mode: number
enable_priority: boolean
no_check_deck: boolean
no_shuffle_deck: boolean
start_lp: number
start_hand: number
draw_count: number
time_limit: number
}
export const ERROR_MSG = StructType({
msg: types.uchar,
code: types.int
});
export interface ERROR_MSG {
msg: number
code: number
}
export const PLAYER_INFO = StructType({
name: ArrayType(types.ushort, 20)
});
export interface PLAYER_INFO {
name: Array<number>
}
export const JOIN_GAME = StructType({
version: types.ushort,
gameid: types.uint,
pass: ArrayType(types.ushort, 20)
});
export interface JOIN_GAME {
version: number,
gameid: number,
pass: Array<number>
}
export const STOC_CHAT = StructType({
player: types.ushort,
msg: ArrayType(types.ushort, 255) // 这里有个迷之bug,客户端定义的长度是256 https://github.com/Fluorohydride/ygopro/blob/master/gframe/network.h#L85 但是发送长度256的数组,客户端会崩,不知道为什么
});
export interface STOC_CHAT {
player: number,
msg: Array<number>
}
export type Struct = HostInfo | ERROR_MSG | PLAYER_INFO | JOIN_GAME | STOC_CHAT
export enum ERRMSG {
JOINERROR = 1,
DECKERROR = 2,
SIDEERROR = 3,
VERERROR = 4
}
export enum COLORS {
LIGHTBLUE = 8,
RED = 11,
GREEN = 12,
BLUE = 13,
BABYBLUE = 14,
PINK = 15,
YELLOW = 16,
WHITE = 17,
GRAY = 18,
DARKGRAY = 19
}
export const STOC = new Map(<[number, StructType][]>Object.entries({
2: ERROR_MSG,
25: STOC_CHAT
}).map(([key, value]) => [parseInt(key), value]));
export const CTOS = new Map<number, StructType>(<[number, StructType][]>Object.entries({
16: PLAYER_INFO,
18: JOIN_GAME
}).map(([key, value]) => [parseInt(key), value]));
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment