Commit ca04a1e9 authored by nanahira's avatar nanahira

rework cloud replay

parent 02164f83
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataManager = void 0;
const moment_1 = __importDefault(require("moment"));
const typeorm_1 = require("typeorm");
const CloudReplay_1 = require("./entities/CloudReplay");
const CloudReplayPlayer_1 = require("./entities/CloudReplayPlayer");
class DataManager {
constructor(config, log) {
this.config = config;
this.ready = false;
this.log = log;
}
async init() {
this.db = await typeorm_1.createConnection({
type: "mysql",
synchronize: true,
entities: ["./data-manager/entities/*.js"],
...this.config
});
this.ready = true;
}
async getCloudReplaysFromKey(key) {
try {
const replays = await this.db.createQueryBuilder(CloudReplay_1.CloudReplay, "replay")
.where("exists (select id from cloud_replay_player where cloud_replay_player.cloudReplayId = replay.id and cloud_replay_player.key = :key)", { key })
.orderBy("replay.date", "DESC")
.limit(10)
.leftJoinAndSelect("replay.players", "player")
.getMany();
return replays;
}
catch (e) {
this.log.warn(`Failed to load replay of ${key}: ${e.toString()}`);
return [];
}
}
async getCloudReplayFromId(id) {
try {
return await this.db.getRepository(CloudReplay_1.CloudReplay).findOne(id, { relations: ["players"] });
}
catch (e) {
this.log.warn(`Failed to load replay R#${id}: ${e.toString()}`);
return null;
}
}
async getRandomCloudReplay() {
try {
return await this.db.createQueryBuilder(CloudReplay_1.CloudReplay, "replay")
.orderBy("rand()")
.limit(4)
.leftJoinAndSelect("replay.players", "player")
.printSql()
.getOne();
}
catch (e) {
this.log.warn(`Failed to load random replay: ${e.toString()}`);
return null;
}
}
async saveCloudReplay(id, buffer, playerInfos) {
const replay = new CloudReplay_1.CloudReplay();
replay.id = id;
replay.fromBuffer(buffer);
replay.date = moment_1.default().toDate();
const players = playerInfos.map(p => {
const player = CloudReplayPlayer_1.CloudReplayPlayer.fromPlayerInfo(p);
return player;
});
await this.db.transaction(async (mdb) => {
try {
const nreplay = await mdb.save(replay);
for (let player of players) {
player.cloudReplay = nreplay;
}
await mdb.save(players);
}
catch (e) {
this.log.warn(`Failed to save replay R#${replay.id}: ${e.toString()}`);
}
});
}
}
exports.DataManager = DataManager;
//# sourceMappingURL=DataManager.js.map
\ No newline at end of file
import moment from "moment";
import { Moment } from "moment";
import bunyan from "bunyan";
import { Connection, ConnectionOptions, createConnection, Transaction } from "typeorm";
import { CloudReplay} from "./entities/CloudReplay";
import { CloudReplayPlayer } from "./entities/CloudReplayPlayer";
export interface CloudReplayPlayerInfo {
name: string;
key: string;
pos: number
}
export class DataManager {
config: ConnectionOptions;
ready: boolean;
db: Connection;
log: bunyan;
constructor(config: ConnectionOptions, log: bunyan) {
this.config = config;
this.ready = false;
this.log = log;
}
async init() {
this.db = await createConnection({
type: "mysql",
synchronize: true,
entities: ["./data-manager/entities/*.js"],
...this.config
});
this.ready = true;
}
async getCloudReplaysFromKey(key: string) {
try {
const replays = await this.db.createQueryBuilder(CloudReplay, "replay")
.where("exists (select id from cloud_replay_player where cloud_replay_player.cloudReplayId = replay.id and cloud_replay_player.key = :key)", { key })
.orderBy("replay.date", "DESC")
.limit(10)
.leftJoinAndSelect("replay.players", "player")
.getMany();
return replays;
} catch (e) {
this.log.warn(`Failed to load replay of ${key}: ${e.toString()}`);
return [];
}
}
async getCloudReplayFromId(id: number) {
try {
return await this.db.getRepository(CloudReplay).findOne(id, {relations: ["players"]});
} catch (e) {
this.log.warn(`Failed to load replay R#${id}: ${e.toString()}`);
return null;
}
}
async getRandomCloudReplay() {
try {
return await this.db.createQueryBuilder(CloudReplay, "replay")
.orderBy("rand()")
.limit(4) //there may be 4 players
.leftJoinAndSelect("replay.players", "player")
.getOne();
} catch (e) {
this.log.warn(`Failed to load random replay: ${e.toString()}`);
return null;
}
}
async saveCloudReplay(id: number, buffer: Buffer, playerInfos: CloudReplayPlayerInfo[]) {
const replay = new CloudReplay();
replay.id = id;
replay.fromBuffer(buffer);
replay.date = moment().toDate();
const players = playerInfos.map(p => {
const player = CloudReplayPlayer.fromPlayerInfo(p);
return player;
});
await this.db.transaction(async (mdb) => {
try {
const nreplay = await mdb.save(replay);
for (let player of players) {
player.cloudReplay = nreplay;
}
await mdb.save(players);
} catch (e) {
this.log.warn(`Failed to save replay R#${replay.id}: ${e.toString()}`);
}
});
}
}
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudReplay = void 0;
const typeorm_1 = require("typeorm");
const CloudReplayPlayer_1 = require("./CloudReplayPlayer");
const underscore_1 = __importDefault(require("underscore"));
const moment_1 = __importDefault(require("moment"));
let CloudReplay = /** @class */ (() => {
let CloudReplay = class CloudReplay {
fromBuffer(buffer) {
this.data = buffer.toString("base64");
}
toBuffer() {
return Buffer.from(this.data, "base64");
}
getDateString() {
return moment_1.default(this.date).format('YYYY-MM-DD HH:mm:ss');
}
getPlayerNamesString() {
const playerInfos = underscore_1.default.clone(this.players);
playerInfos.sort((p1, p2) => p1.pos - p2.pos);
return playerInfos[0].name + (playerInfos[2] ? "+" + playerInfos[2].name : "") + " VS " + (playerInfos[1] ? playerInfos[1].name : "AI") + (playerInfos[3] ? "+" + playerInfos[3].name : "");
}
getDisplayString() {
return `R#${this.id} ${this.getPlayerNamesString()} ${this.getDateString()}`;
}
};
__decorate([
typeorm_1.PrimaryColumn({ unsigned: true, type: "bigint" }),
__metadata("design:type", Number)
], CloudReplay.prototype, "id", void 0);
__decorate([
typeorm_1.Column({ type: "text" }),
__metadata("design:type", String)
], CloudReplay.prototype, "data", void 0);
__decorate([
typeorm_1.Column({ type: "datetime" }),
__metadata("design:type", Date)
], CloudReplay.prototype, "date", void 0);
__decorate([
typeorm_1.OneToMany(() => CloudReplayPlayer_1.CloudReplayPlayer, player => player.cloudReplay),
__metadata("design:type", Array)
], CloudReplay.prototype, "players", void 0);
CloudReplay = __decorate([
typeorm_1.Entity()
], CloudReplay);
return CloudReplay;
})();
exports.CloudReplay = CloudReplay;
//# sourceMappingURL=CloudReplay.js.map
\ No newline at end of file
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { CloudReplayPlayer } from "./CloudReplayPlayer";
import _ from "underscore";
import moment from "moment";
@Entity()
export class CloudReplay {
@PrimaryColumn({ unsigned: true, type: "bigint" })
id: number;
@Column({ type: "text" })
data: string;
fromBuffer(buffer: Buffer) {
this.data = buffer.toString("base64");
}
toBuffer() {
return Buffer.from(this.data, "base64");
}
@Column({ type: "datetime" })
date: Date;
getDateString() {
return moment(this.date).format('YYYY-MM-DD HH:mm:ss')
}
@OneToMany(() => CloudReplayPlayer, player => player.cloudReplay)
players: CloudReplayPlayer[];
getPlayerNamesString() {
const playerInfos = _.clone(this.players);
playerInfos.sort((p1, p2) => p1.pos - p2.pos);
return playerInfos[0].name + (playerInfos[2] ? "+" + playerInfos[2].name : "") + " VS " + (playerInfos[1] ? playerInfos[1].name : "AI") + (playerInfos[3] ? "+" + playerInfos[3].name : "");
}
getDisplayString() {
return `R#${this.id} ${this.getPlayerNamesString()} ${this.getDateString()}`;
}
}
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudReplayPlayer = void 0;
const typeorm_1 = require("typeorm");
const CloudReplay_1 = require("./CloudReplay");
let CloudReplayPlayer = /** @class */ (() => {
var CloudReplayPlayer_1;
let CloudReplayPlayer = CloudReplayPlayer_1 = class CloudReplayPlayer {
static fromPlayerInfo(info) {
const p = new CloudReplayPlayer_1();
p.key = info.key;
p.name = info.name;
p.pos = info.pos;
return p;
}
};
__decorate([
typeorm_1.PrimaryGeneratedColumn({ unsigned: true, type: "bigint" }),
__metadata("design:type", Number)
], CloudReplayPlayer.prototype, "id", void 0);
__decorate([
typeorm_1.Index(),
typeorm_1.Column({ type: "varchar", length: 40 }),
__metadata("design:type", String)
], CloudReplayPlayer.prototype, "key", void 0);
__decorate([
typeorm_1.Column({ type: "varchar", length: 20 }),
__metadata("design:type", String)
], CloudReplayPlayer.prototype, "name", void 0);
__decorate([
typeorm_1.Column({ type: "tinyint" }),
__metadata("design:type", Number)
], CloudReplayPlayer.prototype, "pos", void 0);
__decorate([
typeorm_1.ManyToOne(() => CloudReplay_1.CloudReplay, replay => replay.players),
__metadata("design:type", CloudReplay_1.CloudReplay)
], CloudReplayPlayer.prototype, "cloudReplay", void 0);
CloudReplayPlayer = CloudReplayPlayer_1 = __decorate([
typeorm_1.Entity()
], CloudReplayPlayer);
return CloudReplayPlayer;
})();
exports.CloudReplayPlayer = CloudReplayPlayer;
//# sourceMappingURL=CloudReplayPlayer.js.map
\ No newline at end of file
import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { CloudReplayPlayerInfo } from "../DataManager";
import { CloudReplay } from "./CloudReplay";
@Entity()
export class CloudReplayPlayer {
@PrimaryGeneratedColumn({unsigned: true, type: "bigint"})
id: number;
@Index()
@Column({ type: "varchar", length: 40 })
key: string;
@Column({ type: "varchar", length: 20 })
name: string;
@Column({ type: "tinyint" })
pos: number;
@ManyToOne(() => CloudReplay, replay => replay.players)
cloudReplay: CloudReplay;
static fromPlayerInfo(info: CloudReplayPlayerInfo) {
const p = new CloudReplayPlayer();
p.key = info.key;
p.name = info.name;
p.pos = info.pos;
return p;
}
}
......@@ -82,13 +82,18 @@
"ready_time": 20,
"hang_timeout": 90
},
"cloud_replay": {
"mysql": {
"enabled": false,
"redis": {
"db": {
"host": "127.0.0.1",
"port": 6379
},
"never_expire": false,
"port": 3306,
"username": "root",
"password": "localhost",
"database": "srvpro"
}
},
"cloud_replay": {
"enabled": false,
"enable_halfway_watch": true
},
"windbot": {
......
This diff is collapsed.
......@@ -11,23 +11,27 @@
],
"author": "zh99998 <zh99998@gmail.com>, mercury233 <me@mercury233.me>, Nanahira <78877@qq.com>",
"dependencies": {
"@types/bunyan": "^1.8.6",
"@types/node": "^14.0.13",
"@types/underscore": "^1.10.0",
"async": "^3.2.0",
"axios": "^0.19.2",
"bunyan": "latest",
"bunyan": "^1.8.14",
"challonge": "latest",
"deepmerge": "latest",
"formidable": "latest",
"geoip-country-lite": "latest",
"load-json-file": "latest",
"lzma": "^2.3.2",
"moment": "latest",
"moment": "^2.29.1",
"mysql": "^2.18.1",
"pg": "^6.4.2",
"q": "^1.5.1",
"querystring": "^0.2.0",
"redis": "latest",
"request": "latest",
"sqlite3": "latest",
"typeorm": "^0.2.29",
"underscore": "latest",
"underscore.string": "latest",
"ws": "^1.1.1"
......
......@@ -4,11 +4,15 @@
"target": "esnext",
"esModuleInterop": true,
"resolveJsonModule": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true
},
"compileOnSave": true,
"allowJs": true,
"include": [
"*.ts"
"*.ts",
"data-manager/*.ts",
"data-manager/entities/*.ts"
]
}
......@@ -20,6 +20,7 @@ _.mixin(_.str.exports())
request = require 'request'
axios = require 'axios'
qs = require "querystring"
zlib = require 'zlib'
bunyan = require 'bunyan'
log = global.log = bunyan.createLogger name: "mycard"
......@@ -84,6 +85,8 @@ loadJSON = require('load-json-file').sync
util = require("util")
Q = require("q")
#heapdump = require 'heapdump'
# 配置
......@@ -181,11 +184,6 @@ imported = false
if settings.modules.http.quick_death_rule == true
settings.modules.http.quick_death_rule = 1
imported = true
#import the old redis port
if settings.modules.cloud_replay.redis_port
settings.modules.cloud_replay.redis.port = settings.modules.cloud_replay.redis_port
delete settings.modules.cloud_replay.redis_port
imported = true
#import the old passwords to new admin user system
if settings.modules.http.password
auth.add_user("olduser", settings.modules.http.password, true, {
......@@ -299,14 +297,6 @@ try
lflists.push({date: moment(list.match(/!([\d\.]+)/)[1], 'YYYY.MM.DD').utcOffset("-08:00"), tcg: list.indexOf('TCG') != -1})
catch
if settings.modules.cloud_replay.enabled
redis = require 'redis'
zlib = require 'zlib'
redisdb = global.redisdb = redis.createClient(settings.modules.cloud_replay.redis)
redisdb.on 'error', (err)->
log.warn err
return
if settings.modules.windbot.enabled
windbots = global.windbots = loadJSON(settings.modules.windbot.botlist).windbots
real_windbot_server_ip = global.real_windbot_server_ip = settings.modules.windbot.server_ip
......@@ -338,6 +328,11 @@ if settings.modules.i18n.auto_pick
# cache users of mycard login
users_cache = {}
if settings.modules.mysql.enabled
DataManager = require('./data-manager/DataManager.js').DataManager
dataManager = new DataManager(settings.modules.mysql.db, log)
dataManager.init().then(() -> log.info("Database ready."))
if settings.modules.mycard.enabled
pgClient = require('pg').Client
pg_client = global.pg_client = new pgClient(settings.modules.mycard.auth_database)
......@@ -1358,33 +1353,12 @@ class Room
replay_id = @cloud_replay_id
if @has_ygopro_error
log_rep_id = true
player_names=@player_datas[0].name + (if @player_datas[2] then "+" + @player_datas[2].name else "") +
" VS " +
(if @player_datas[1] then @player_datas[1].name else "AI") +
(if @player_datas[3] then "+" + @player_datas[3].name else "")
player_ips=[]
_.each @player_datas, (player)->
player_ips.push(player.key)
return
recorder_buffer=Buffer.concat(@recorder_buffers)
player_datas = @player_datas
zlib.deflate recorder_buffer, (err, replay_buffer) ->
replay_buffer=replay_buffer.toString('binary')
#log.info err, replay_buffer
date_time=moment().format('YYYY-MM-DD HH:mm:ss')
#replay_id=Math.floor(Math.random()*100000000)
redisdb.hmset("replay:"+replay_id,
"replay_id", replay_id,
"replay_buffer", replay_buffer,
"player_names", player_names,
"date_time", date_time)
if !log_rep_id and !settings.modules.cloud_replay.never_expire
redisdb.expire("replay:"+replay_id, 60*60*24)
recorded_ip=[]
_.each player_ips, (player_ip)->
return if _.contains(recorded_ip, player_ip)
recorded_ip.push player_ip
redisdb.lpush(player_ip+":replays", replay_id)
return
dataManager.saveCloudReplay(replay_id, replay_buffer, player_datas).catch((err) ->
log.warn("Replay save error: R##{replay_id} #{err.toString()}")
)
if log_rep_id
log.info "error replay: R#" + replay_id
return
......@@ -1725,22 +1699,21 @@ net.createServer (client) ->
return
if settings.modules.cloud_replay.enabled
client.open_cloud_replay= (err, replay)->
if err or !replay
client.open_cloud_replay = (replay)->
if !replay
ygopro.stoc_die(client, "${cloud_replay_no}")
return
redisdb.expire("replay:"+replay.replay_id, 60*60*48)
buffer=Buffer.from(replay.replay_buffer,'binary')
zlib.unzip buffer, (err, replay_buffer) ->
if err
log.info "cloud replay unzip error: " + err
ygopro.stoc_send_chat(client, "${cloud_replay_error}", ygopro.constants.COLORS.RED)
CLIENT_kick(client)
return
ygopro.stoc_send_chat(client, "${cloud_replay_playing} R##{replay.replay_id} #{replay.player_names} #{replay.date_time}", ygopro.constants.COLORS.BABYBLUE)
client.write replay_buffer, ()->
CLIENT_kick(client)
return
buffer=replay.toBuffer()
replay_buffer = null
try
replay_buffer = await util.promisify(zlib.unzip)(buffer)
catch e
log.info "cloud replay unzip error: " + err
ygopro.stoc_die(client, "${cloud_replay_error}")
return
ygopro.stoc_send_chat(client, "${cloud_replay_playing} #{replay.getDisplayString()}", ygopro.constants.COLORS.BABYBLUE)
client.write replay_buffer, ()->
CLIENT_kick(client)
return
return
......@@ -1891,24 +1864,14 @@ ygopro.ctos_follow 'JOIN_GAME', true, (buffer, info, client, server, datas)->
else if info.pass.toUpperCase()=="R" and settings.modules.cloud_replay.enabled
ygopro.stoc_send_chat(client,"${cloud_replay_hint}", ygopro.constants.COLORS.BABYBLUE)
redisdb.lrange CLIENT_get_authorize_key(client)+":replays", 0, 2, (err, result)->
_.each result, (replay_id,id)->
redisdb.hgetall "replay:"+replay_id, (err, replay)->
if err or !replay
log.info "cloud replay getall error: " + err if err
return
ygopro.stoc_send_chat(client,"<#{id-0+1}> R##{replay_id} #{replay.player_names} #{replay.date_time}", ygopro.constants.COLORS.BABYBLUE)
return
return
return
# 强行等待异步执行完毕_(:з」∠)_
setTimeout (()->
ygopro.stoc_send client, 'ERROR_MSG',{
msg: 1
code: 9
}
CLIENT_kick(client)
return), 500
replays = await dataManager.getCloudReplaysFromKey(CLIENT_get_authorize_key(client))
for replay,index in replays
ygopro.stoc_send_chat(client,"<#{index + 1}> #{replay.getDisplayString()}", ygopro.constants.COLORS.BABYBLUE)
ygopro.stoc_send client, 'ERROR_MSG', {
msg: 1
code: 9
}
CLIENT_kick(client)
else if info.pass.toUpperCase()=="RC" and settings.modules.tournament_mode.enable_recover
ygopro.stoc_send_chat(client,"${recover_replay_hint}", ygopro.constants.COLORS.BABYBLUE)
......@@ -1935,22 +1898,12 @@ ygopro.ctos_follow 'JOIN_GAME', true, (buffer, info, client, server, datas)->
else if info.pass[0...2].toUpperCase()=="R#" and settings.modules.cloud_replay.enabled
replay_id=info.pass.split("#")[1]
if (replay_id>0 and replay_id<=9)
redisdb.lindex client.ip+":replays", replay_id-1, (err, replay_id)->
if err or !replay_id
log.info "cloud replay replayid error: " + err if err
ygopro.stoc_die(client, "${cloud_replay_no}")
return
redisdb.hgetall "replay:"+replay_id, client.open_cloud_replay
return
else if replay_id
redisdb.hgetall "replay:"+replay_id, client.open_cloud_replay
else
ygopro.stoc_die(client, "${cloud_replay_no}")
replay = await dataManager.getCloudReplayFromId(replay_id)
await client.open_cloud_replay(replay)
else if info.pass.toUpperCase()=="W" and settings.modules.cloud_replay.enabled
replay_id=Cloud_replay_ids[Math.floor(Math.random()*Cloud_replay_ids.length)]
redisdb.hgetall "replay:"+replay_id, client.open_cloud_replay
replay = await dataManager.getRandomCloudReplay()
await client.open_cloud_replay(replay)
else if info.version != settings.version and !settings.alternative_versions.includes(info.version)
ygopro.stoc_send_chat(client, settings.modules.update, ygopro.constants.COLORS.RED)
......@@ -2969,10 +2922,10 @@ ygopro.stoc_follow 'DUEL_START', false, (buffer, info, client, server, datas)->
roomlist.start room if !room.windbot and settings.modules.http.websocket_roomlist
#room.duels = []
room.dueling_players = []
for player in room.players when player.pos != 7
for player in room.get_playing_player()
room.dueling_players[player.pos] = player
room.scores[player.name_vpass] = 0
room.player_datas.push key: CLIENT_get_authorize_key(player), name: player.name
room.player_datas.push key: CLIENT_get_authorize_key(player), name: player.name, pos: player.pos
if room.random_type == 'T'
# 双打房不记录匹配过
ROOM_players_oppentlist[player.ip] = null
......
This diff is collapsed.
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