375 lines
11 KiB
JavaScript
375 lines
11 KiB
JavaScript
|
const commandLineArgs = require('command-line-args'),
|
||
|
commandLineUsage = require('command-line-usage'),
|
||
|
https = require('https'),
|
||
|
qstr = require('querystring'),
|
||
|
url = require('url'),
|
||
|
fs = require('fs'),
|
||
|
pbjs = require("protobufjs/cli/pbjs"),
|
||
|
WebSocketClient = require('websocket').client,
|
||
|
MS_HOST = 'mahjongsoul.game.yo-star.com',
|
||
|
opts = [{
|
||
|
name: 'help',
|
||
|
alias: 'h',
|
||
|
description: 'Display this usage guide',
|
||
|
type: Boolean
|
||
|
}, {
|
||
|
name: 'servers',
|
||
|
alias: 'l',
|
||
|
description: 'Get server list',
|
||
|
type: Boolean
|
||
|
}, {
|
||
|
name: 'server',
|
||
|
alias: 's',
|
||
|
description: 'Use custom server within check',
|
||
|
type: String
|
||
|
}, {
|
||
|
name: 'trueserver',
|
||
|
alias: 'S',
|
||
|
description: 'Use custom server without check',
|
||
|
type: String
|
||
|
}, {
|
||
|
name: 'update',
|
||
|
alias: 'u',
|
||
|
description: 'Update protocol',
|
||
|
type: Boolean
|
||
|
}, {
|
||
|
name: 'info',
|
||
|
alias: 'i',
|
||
|
description: 'Get info',
|
||
|
type: Boolean
|
||
|
}, {
|
||
|
name: 'docs',
|
||
|
alias: 'd',
|
||
|
description: 'Generate documentation from liqi.json',
|
||
|
type: Boolean
|
||
|
}],
|
||
|
sections = [{
|
||
|
header: 'Mahjong Soul RPC client',
|
||
|
content: 'RPC client for Mahjong Soul API'
|
||
|
}, {
|
||
|
header: 'Options',
|
||
|
optionList: opts
|
||
|
}],
|
||
|
args = commandLineArgs(opts);
|
||
|
let ProtoBuf = require("protobufjs");
|
||
|
|
||
|
const getServers = (serv = null, nocheck = false) => {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
if (serv && nocheck) {
|
||
|
resolve([serv]);
|
||
|
} else {
|
||
|
let ms_opts = {
|
||
|
hostname: MS_HOST,
|
||
|
port: 443,
|
||
|
path: '/version.json',
|
||
|
method: 'GET'
|
||
|
};
|
||
|
// Запрос версии
|
||
|
const reqVersion = https.request(ms_opts, (resVersion) => {
|
||
|
resVersion.on('data', (dataVersion) => {
|
||
|
let resVers = JSON.parse(dataVersion);
|
||
|
//console.log('Version:', JSON.stringify(resVers, null, 2));
|
||
|
ms_opts.path = `/v${resVers.version}/config.json`;
|
||
|
// Запрос конфигурации
|
||
|
const reqConfig = https.request(ms_opts, (resConfig) => {
|
||
|
resConfig.on('data', (dataConfig) => {
|
||
|
let resCfg = JSON.parse(dataConfig);
|
||
|
//console.log('Config:', JSON.stringify(resCfg, null, 2));
|
||
|
const mainland = url.parse(resCfg.ip[0].region_urls[0]);
|
||
|
ms_opts.hostname = mainland.hostname;
|
||
|
ms_opts.port = mainland.port;
|
||
|
ms_opts.data = {
|
||
|
service: 'ws-gateway',
|
||
|
protocol: 'ws',
|
||
|
ssl: 'true'
|
||
|
};
|
||
|
ms_opts.path = `${mainland.pathname}?${qstr.stringify(ms_opts.data)}`;
|
||
|
// Запрос серверов
|
||
|
const reqServers = https.request(ms_opts, (resServers) => {
|
||
|
resServers.on('data', (dataServers) => {
|
||
|
let resServs = JSON.parse(dataServers);
|
||
|
//console.log('Servers:', JSON.stringify(resServs, null, 2));
|
||
|
if (serv && !nocheck) {
|
||
|
if (resServs.servers.indexOf(serv) >= 0) {
|
||
|
resolve([serv]);
|
||
|
} else {
|
||
|
reject(`Wrong server: ${serv}`);
|
||
|
}
|
||
|
} else {
|
||
|
resolve(resServs.servers);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
reqServers.on('error', (e) => {
|
||
|
reject(`Error: ${e.message}`);
|
||
|
});
|
||
|
reqServers.end();
|
||
|
});
|
||
|
});
|
||
|
reqConfig.on('error', (e) => {
|
||
|
reject(`Error: ${e.message}`);
|
||
|
});
|
||
|
reqConfig.end();
|
||
|
});
|
||
|
});
|
||
|
reqVersion.on('error', (e) => {
|
||
|
reject(`Error: ${e.message}`);
|
||
|
});
|
||
|
reqVersion.end();
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// version URL https://mahjongsoul.game.yo-star.com/version.json
|
||
|
const getLastVersion = () => {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
let ms_opts = {
|
||
|
hostname: MS_HOST,
|
||
|
port: 443,
|
||
|
path: '/version.json',
|
||
|
method: 'GET'
|
||
|
};
|
||
|
const reqVersion = https.request(ms_opts, (resVersion) => {
|
||
|
let dataVersion = '';
|
||
|
resVersion.on('data', (chunk) => {
|
||
|
dataVersion += chunk;
|
||
|
});
|
||
|
resVersion.on('end', () => {
|
||
|
let resVers = JSON.parse(dataVersion);
|
||
|
resolve(resVers.version);
|
||
|
});
|
||
|
});
|
||
|
reqVersion.on('error', (e) => {
|
||
|
reject(`Error: ${e.message}`);
|
||
|
});
|
||
|
reqVersion.end();
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const sendRequest = (server, package_name, service_name, method_name, params, idx = 1) => {
|
||
|
return new Promise((resolve, reject) => {
|
||
|
const proto = ProtoBuf.loadSync('liqi.json'),
|
||
|
client = new WebSocketClient(),
|
||
|
service = proto.lookup(`${package_name}.${service_name}`),
|
||
|
method = service.methods[method_name].resolve(),
|
||
|
req = method.resolvedRequestType,
|
||
|
res = method.resolvedResponseType,
|
||
|
wrapper = proto.lookupType(`${package_name}.Wrapper`);
|
||
|
let req_str = wrapper.create({
|
||
|
//name: `${req.name}`,
|
||
|
name: `${package_name}.${req.name}`,
|
||
|
//data: params
|
||
|
data: req.encode(params).finish()
|
||
|
});
|
||
|
let meth_str = wrapper.create({
|
||
|
name: `${package_name}.${service_name}.${method_name}`,
|
||
|
data: wrapper.encode(params).finish()
|
||
|
//data: wrapper.encode(req_str).finish()
|
||
|
});
|
||
|
let msg = wrapper.encode(meth_str).finish(),
|
||
|
pkt_type = [Number(2)],
|
||
|
idx_bytes = to_bytes(idx);
|
||
|
let header = pkt_type.concat(idx_bytes),
|
||
|
pkt = Buffer.concat([Buffer.from(header), Buffer.from(msg)]);
|
||
|
console.log(`method: .${package_name}.${service_name}.${method_name}`);
|
||
|
console.log(`request: ${JSON.stringify(params, null, 2)}`);
|
||
|
client.on('connect', function(connection) {
|
||
|
console.log('WebSocket Client Connected');
|
||
|
connection.on('error', function(error) {
|
||
|
reject(error.toString());
|
||
|
});
|
||
|
connection.on('close', function() {
|
||
|
console.log('Connection Closed');
|
||
|
});
|
||
|
connection.on('message', function(message) {
|
||
|
console.log(message);
|
||
|
if (message.type === 'binary') {
|
||
|
let r = message.binaryData.slice(3),
|
||
|
msg = res.decode(r);
|
||
|
resolve(msg);
|
||
|
connection.close(connection.CLOSE_REASON_NORMAL, 'Close');
|
||
|
}
|
||
|
});
|
||
|
const sendPacket = (packet) => {
|
||
|
if (connection.connected) {
|
||
|
console.log('Send data: ', packet.toString('hex'));
|
||
|
connection.sendBytes(packet);
|
||
|
}
|
||
|
};
|
||
|
sendPacket(pkt);
|
||
|
});
|
||
|
client.connect(`wss://${server}`);
|
||
|
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const random = (arr) => {
|
||
|
return arr[Math.floor((Math.random() * arr.length))];
|
||
|
};
|
||
|
|
||
|
const to_bytes = (x) => {
|
||
|
return [(x & 255), x >> 8];
|
||
|
};
|
||
|
|
||
|
if (!args || args.help) {
|
||
|
console.log(commandLineUsage(sections));
|
||
|
} else {
|
||
|
if (args.update) {
|
||
|
console.log('Protocol update:');
|
||
|
getLastVersion().then(
|
||
|
resGameVersion => {
|
||
|
console.log(` Game version: ${resGameVersion}`);
|
||
|
// Resource version URL https://mahjongsoul.game.yo-star.com/resversion(version).json
|
||
|
const resvers_opts = {
|
||
|
hostname: MS_HOST,
|
||
|
port: 443,
|
||
|
path: `/resversion${resGameVersion}.json`,
|
||
|
method: 'GET'
|
||
|
};
|
||
|
const reqVersion = https.request(resvers_opts, (resVersion) => {
|
||
|
let dataProto = '';
|
||
|
resVersion.on('data', (chunk) => {
|
||
|
dataProto += chunk;
|
||
|
});
|
||
|
resVersion.on('end', () => {
|
||
|
const resJSON = JSON.parse(dataProto),
|
||
|
liqiVersion = resJSON.res['res/proto/liqi.json'].prefix;
|
||
|
let res_list = '';
|
||
|
Object.keys(resJSON.res).forEach((res_id) => {
|
||
|
res_list += `https://${MS_HOST}/${resJSON.res[res_id].prefix}/${res_id}\n`;
|
||
|
});
|
||
|
fs.writeFileSync(`${__dirname}/res_list.txt`, res_list);
|
||
|
console.log(' Save res_list.txt');
|
||
|
console.log(` Liqi version: ${liqiVersion}`);
|
||
|
// Resource version URL https://mahjongsoul.game.yo-star.com/(versionLiqi)/res/proto/liqi.json
|
||
|
const proto_opts = {
|
||
|
hostname: MS_HOST,
|
||
|
port: 443,
|
||
|
path: `/${liqiVersion}/res/proto/liqi.json`,
|
||
|
method: 'GET'
|
||
|
};
|
||
|
const reqProto = https.request(proto_opts, (resProto) => {
|
||
|
let dataProto = '';
|
||
|
resProto.on('data', (chunk) => {
|
||
|
dataProto += chunk;
|
||
|
});
|
||
|
resProto.on('end', () => {
|
||
|
let resJSON = JSON.parse(dataProto);
|
||
|
fs.writeFileSync(`${__dirname}/liqi.json`, JSON.stringify(resJSON, null, 2));
|
||
|
console.log(' Save liqi.json');
|
||
|
// Если нужен proto раскомментить
|
||
|
/*
|
||
|
pbjs.main(['-t', 'proto3', `${__dirname}/liqi.json`, '-o', `${__dirname}/liqi.proto`], function(err, output) {
|
||
|
if (err) throw err;
|
||
|
console.log(' Save to proto');
|
||
|
});
|
||
|
*/
|
||
|
});
|
||
|
});
|
||
|
reqProto.on('error', (e) => {
|
||
|
console.log(`Error: ${e.message}`);
|
||
|
});
|
||
|
reqProto.end();
|
||
|
|
||
|
});
|
||
|
});
|
||
|
reqVersion.on('error', (e) => {
|
||
|
console.log(`Error: ${e.message}`);
|
||
|
});
|
||
|
reqVersion.end();
|
||
|
},
|
||
|
errGameVersion => {
|
||
|
console.log(`Error: ${errGameVersion}`);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
if (args.servers) {
|
||
|
getServers().then(
|
||
|
resServer => {
|
||
|
console.log('Server list:');
|
||
|
for (let i in resServer) {
|
||
|
console.log(` ${resServer[i]}`);
|
||
|
}
|
||
|
console.log(`Random server: ${random(resServer)}`);
|
||
|
},
|
||
|
errServer => {
|
||
|
console.error(errServer);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
if (args.info) {
|
||
|
if (!fs.existsSync(`${__dirname}/liqi.json`)) {
|
||
|
console.log('Protocol file not found. Run update with option --update');
|
||
|
} else {
|
||
|
getServers((args.server ? args.server : args.trueserver), args.trueserver).then(
|
||
|
resServer => {
|
||
|
let server = random(resServer);
|
||
|
if (server) {
|
||
|
console.log(`Use server: ${server}`);
|
||
|
let idx = Math.floor(60007 * Math.random()),
|
||
|
params = {
|
||
|
//account_id: 117411310
|
||
|
code: '526091',
|
||
|
operation: 0
|
||
|
};
|
||
|
//sendRequest(server, 'lq', 'Lobby', 'fetchConnectionInfo', params, idx).then(
|
||
|
sendRequest(server, 'lq', 'Lobby', 'verfifyCodeForSecure', params, idx).then(
|
||
|
resSend => {
|
||
|
console.log(`Result: ${JSON.stringify(resSend, null, 2)}`);
|
||
|
},
|
||
|
errSend => {
|
||
|
console.error(`Error: ${errSend}`);
|
||
|
}
|
||
|
);
|
||
|
} else {
|
||
|
console.error(`Wrong server: ${args.server}`);
|
||
|
}
|
||
|
},
|
||
|
errServer => {
|
||
|
console.error(errServer);
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
if (args.docs) {
|
||
|
if (!fs.existsSync(`${__dirname}/liqi.json`)) {
|
||
|
console.log('Protocol file not found. Run update with option --update');
|
||
|
} else {
|
||
|
const liqi = require(`${__dirname}/liqi.json`),
|
||
|
types = new RegExp(/uint32|bool|string|int32/);
|
||
|
let doc = "# MahjongSoul protocol documentation\n\n"
|
||
|
items = liqi.nested.lq.nested;
|
||
|
for (const [key, item] of Object.entries(items)) {
|
||
|
doc += `## ${key}\n\n`;
|
||
|
if (item.fields) {
|
||
|
let fields = [];
|
||
|
for (let [name, field] of Object.entries(item.fields)) {
|
||
|
fields[field.id] = {
|
||
|
name: name,
|
||
|
type: field.type
|
||
|
};
|
||
|
}
|
||
|
if (fields.length > 0) {
|
||
|
doc += "|#|Field name|Field type|\n|:-:|-|-|\n";
|
||
|
for (let i = 1; i <= fields.length; i++) {
|
||
|
if (fields[i]) {
|
||
|
doc += `|${i}|${fields[i].name}|${(types.test(fields[i].type) ? fields[i].type : '['+fields[i].type+'](#'+fields[i].type+')')}|\n`;
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
doc += "No fields\n";
|
||
|
}
|
||
|
doc += "\n";
|
||
|
} else if (item.methods) {
|
||
|
const methods = item.methods;
|
||
|
for (const [key, method] of Object.entries(methods)) {
|
||
|
doc += `* ${key}([${method.responseType}](#${method.responseType}) return [${method.requestType}](#${method.requestType})\n`;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
fs.writeFileSync(`${__dirname}/liqi.md`, doc);
|
||
|
console.log('Save liqi.md');
|
||
|
}
|
||
|
}
|
||
|
}
|