master
root 2020-06-26 12:37:28 +07:00
parent 6e0224102b
commit 1a8c5c72d7
7 changed files with 167609 additions and 0 deletions

139421
code.js Executable file

File diff suppressed because one or more lines are too long

375
index.js Normal file
View File

@ -0,0 +1,375 @@
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');
}
}
}

10043
liqi.json Normal file

File diff suppressed because it is too large Load Diff

3986
liqi.md Normal file

File diff suppressed because it is too large Load Diff

3999
liqi.proto Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "ms",
"version": "1.0.0",
"description": "Mahjong Soul RPC client",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"bytebuffer": "^5.0.1",
"command-line-args": "^5.1.1",
"command-line-usage": "^6.0.2",
"protobufjs": "^6.8.8",
"websocket": "^1.0.30"
}
}

9767
res_list.txt Normal file

File diff suppressed because it is too large Load Diff