First
This commit is contained in:
commit
052db244e6
22 changed files with 2535 additions and 0 deletions
7
.env.example
Normal file
7
.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
ACCOUNT=graal1234
|
||||
PASSWORD=RQWUEIBAS
|
||||
# The following are optional
|
||||
# Remove the # to enable
|
||||
|
||||
# Discord Webhook to send logs from RC to Discord
|
||||
#RC_LOG_DISCORD_WEBHOOK=
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
package-lock.json
|
||||
.env
|
||||
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true
|
||||
}
|
||||
22
package.json
Normal file
22
package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "ruan-graal-rc",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"license": "ISC",
|
||||
"author": "Ruan Fernandes",
|
||||
"type": "commonjs",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"ndmon": "nodemon src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.14",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"chalk": "^5.4.1",
|
||||
"compressjs": "^1.0.3",
|
||||
"dotenv": "^16.4.7"
|
||||
}
|
||||
}
|
||||
203
src/FileBrowser.ts
Normal file
203
src/FileBrowser.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { FolderRights } from './FolderRights';
|
||||
import { GBufferReader, GBufferWriter } from './GBuffer';
|
||||
import { PromiseManager } from './PromiseManager';
|
||||
import { RCOutgoingPacket } from './misc/packet';
|
||||
import { RemoteControl } from './RemoteControl';
|
||||
import * as types from './types';
|
||||
|
||||
export interface FileBrowser {
|
||||
cd(folder: string): PromiseLike<types.DirectoryListing>;
|
||||
get(file: string): PromiseLike<Buffer>;
|
||||
put(file: string, content: Buffer): void;
|
||||
}
|
||||
|
||||
type FileStructure = {
|
||||
name: string;
|
||||
size: number;
|
||||
modTime: number;
|
||||
buffer: Buffer[];
|
||||
};
|
||||
|
||||
type FileData = { [name: string]: FileStructure };
|
||||
|
||||
export class RCFileBrowser implements FileBrowser {
|
||||
private folderRights: FolderRights;
|
||||
private promiseManager: PromiseManager;
|
||||
private fileData: FileData = {};
|
||||
private active: string = '';
|
||||
|
||||
constructor(private readonly rc: RemoteControl) {
|
||||
this.folderRights = new FolderRights();
|
||||
this.promiseManager = new PromiseManager();
|
||||
}
|
||||
|
||||
/////////
|
||||
|
||||
setFolderRights(content: string) {
|
||||
this.folderRights.setFolderRights(content);
|
||||
}
|
||||
|
||||
setFolderFiles(reader: GBufferReader, folderName: string) {
|
||||
const dirList: types.DirectoryListing = {
|
||||
directory: folderName,
|
||||
fileList: this.folderRights.getSubFolders(folderName),
|
||||
};
|
||||
|
||||
while (reader.bytesLeft) {
|
||||
reader.readUInt8(); // skip space
|
||||
|
||||
const reader2 = reader.readGBuffer();
|
||||
dirList.fileList.push({
|
||||
name: reader2.readGString(),
|
||||
type: types.FSEntryType.File,
|
||||
permissions: reader2.readGString(),
|
||||
fileSize: reader2.readGULong(),
|
||||
modTime: reader2.readGULong(),
|
||||
});
|
||||
}
|
||||
|
||||
this.promiseManager.resolvePromise('dir://' + folderName, dirList);
|
||||
}
|
||||
|
||||
startLargeFile(fileName: string) {
|
||||
this.fileData[fileName] = {
|
||||
name: fileName,
|
||||
size: 0,
|
||||
modTime: 0,
|
||||
buffer: [],
|
||||
};
|
||||
|
||||
this.active = fileName;
|
||||
}
|
||||
|
||||
setActiveFileSize(size: number) {
|
||||
if (this.active in this.fileData) {
|
||||
this.fileData[this.active].size = size;
|
||||
}
|
||||
}
|
||||
|
||||
appendFileContent(fileName: string, modTime: number, buffer: Buffer) {
|
||||
if (fileName in this.fileData) {
|
||||
this.fileData[fileName].modTime = modTime;
|
||||
this.fileData[fileName].buffer.push(buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
this.startLargeFile(fileName);
|
||||
this.setActiveFileSize(buffer.length);
|
||||
|
||||
this.fileData[fileName].modTime = modTime;
|
||||
this.fileData[fileName].buffer.push(buffer);
|
||||
|
||||
this.finishLargeFile(fileName);
|
||||
}
|
||||
|
||||
finishLargeFile(fileName: string) {
|
||||
if (fileName in this.fileData) {
|
||||
this.promiseManager.resolvePromise(
|
||||
fileName,
|
||||
Buffer.concat(this.fileData[fileName].buffer),
|
||||
);
|
||||
delete this.fileData[fileName];
|
||||
}
|
||||
|
||||
if (fileName == this.active) {
|
||||
this.active = '';
|
||||
}
|
||||
}
|
||||
|
||||
fileDownloadFailed(fileName: string) {
|
||||
if (fileName in this.fileData) {
|
||||
this.promiseManager.rejectPromise(fileName, 'File download failed');
|
||||
delete this.fileData[fileName];
|
||||
}
|
||||
|
||||
if (fileName == this.active) {
|
||||
this.active = '';
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
cd(folder: string): Promise<types.DirectoryListing> {
|
||||
if (!folder) {
|
||||
return new Promise((resolve) => {
|
||||
resolve({
|
||||
directory: '/',
|
||||
fileList: this.folderRights.getSubFolders(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!folder.endsWith('/')) {
|
||||
folder += '/';
|
||||
}
|
||||
|
||||
this.rc.socket?.sendData(
|
||||
this.rc.socket?.sendPacket(
|
||||
RCOutgoingPacket.PLI_RC_FILEBROWSER_CD,
|
||||
Buffer.from(folder),
|
||||
),
|
||||
);
|
||||
return this.promiseManager.createPromise('dir://' + folder);
|
||||
}
|
||||
|
||||
put(fileName: string, content: Buffer): void {
|
||||
if (content.length > this.rc.maxUploadFileSize) {
|
||||
throw new Error('File is too large!');
|
||||
}
|
||||
|
||||
const largeFileThreshold = 32000;
|
||||
const isLargeFile = content.length > largeFileThreshold;
|
||||
|
||||
if (isLargeFile) {
|
||||
this.rc.socket?.sendData(
|
||||
this.rc.socket?.sendPacket(
|
||||
RCOutgoingPacket.PLI_RC_LARGEFILESTART,
|
||||
Buffer.from(fileName),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const reader = GBufferReader.from(content);
|
||||
while (reader.bytesLeft) {
|
||||
const cur = reader.read(largeFileThreshold);
|
||||
const dataSize = 3 + fileName.length + cur.length; // char(PLI_RC_FILEBROWSER_UP) + len(file) + file + cur + '\n'
|
||||
const writer = GBufferWriter.create(4 + dataSize); // len(data) + '\n'
|
||||
|
||||
writer.writeGUInt24(dataSize);
|
||||
writer.writeUInt8(0xa);
|
||||
|
||||
writer.writeGUInt8(RCOutgoingPacket.PLI_RC_FILEBROWSER_UP);
|
||||
writer.writeGString(fileName);
|
||||
writer.writeBuffer(cur);
|
||||
writer.writeUInt8(0xa);
|
||||
|
||||
this.rc.socket?.sendData(
|
||||
this.rc.socket?.sendPacket(
|
||||
RCOutgoingPacket.PLI_RAWDATA,
|
||||
writer.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isLargeFile) {
|
||||
this.rc.socket?.sendData(
|
||||
this.rc.socket?.sendPacket(
|
||||
RCOutgoingPacket.PLI_RC_LARGEFILEEND,
|
||||
Buffer.from(fileName),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get(fileName: string): PromiseLike<Buffer> {
|
||||
this.rc.socket?.sendData(
|
||||
this.rc.socket?.sendPacket(
|
||||
RCOutgoingPacket.PLI_RC_FILEBROWSER_DOWN,
|
||||
Buffer.from(fileName),
|
||||
),
|
||||
);
|
||||
return this.promiseManager.createPromise(fileName);
|
||||
}
|
||||
}
|
||||
73
src/FolderRights.ts
Normal file
73
src/FolderRights.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import * as types from './types';
|
||||
import * as utils from './utils';
|
||||
|
||||
type FolderRightsData = {
|
||||
folder: string;
|
||||
permissions: string;
|
||||
match: string;
|
||||
};
|
||||
|
||||
type FolderRightsMap = { [name: string]: FolderRightsData };
|
||||
|
||||
export class FolderRights {
|
||||
private folderRightsText: string = '';
|
||||
private folderRights: FolderRightsMap = {};
|
||||
|
||||
public setFolderRights(rights: string) {
|
||||
const re = /^([r]?[w]?) (.*)\/(.*)$/gm;
|
||||
const matches = rights.matchAll(re);
|
||||
|
||||
this.folderRightsText = '';
|
||||
this.folderRights = {};
|
||||
|
||||
for (const match of matches) {
|
||||
let [, perm, folder, filematch] = match;
|
||||
|
||||
this.folderRights[folder] = {
|
||||
folder: folder,
|
||||
permissions: perm,
|
||||
match: filematch,
|
||||
};
|
||||
|
||||
this.folderRightsText += folder + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get immediate subfolders based on the users folder rights
|
||||
*
|
||||
* @param folder
|
||||
* @returns
|
||||
*/
|
||||
public getSubFolders(folder: string = ''): types.FSEntries[] {
|
||||
if (folder && !folder.endsWith('/')) {
|
||||
folder += '/';
|
||||
}
|
||||
|
||||
const escapedFolder = utils.escapeRegExp(folder);
|
||||
const regex = new RegExp(`^${escapedFolder}([^\/\n]+)`, 'gm');
|
||||
const matches = this.folderRightsText.matchAll(regex);
|
||||
|
||||
const entries: { [key: string]: types.FSEntries } = {};
|
||||
|
||||
for (const match of matches) {
|
||||
const curFolder = match[1];
|
||||
|
||||
if (!(curFolder in entries)) {
|
||||
const folderPath = folder + curFolder;
|
||||
|
||||
if (folderPath in this.folderRights) {
|
||||
entries[curFolder] = {
|
||||
name: curFolder,
|
||||
type: types.FSEntryType.Directory,
|
||||
permissions: this.folderRights[folderPath].permissions,
|
||||
fileSize: 0,
|
||||
modTime: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(entries);
|
||||
}
|
||||
}
|
||||
267
src/GBuffer.ts
Normal file
267
src/GBuffer.ts
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
const DEFAULT_BUFFER_SIZE = 32;
|
||||
|
||||
function decodeBits(b: Buffer) {
|
||||
let res = 0;
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
res <<= 7;
|
||||
res |= b[i] - 32;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function encodeBits(v: number, b: number[]) {
|
||||
for (let i = b.length; i > 0; i--) {
|
||||
b[i - 1] = (v & 0x7f) + 32;
|
||||
v >>= 7;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BufferReader over an existing buffer, does not copy the buffer
|
||||
*
|
||||
* Allows easily reading packets
|
||||
*/
|
||||
export class GBufferReader {
|
||||
private buffer: Buffer;
|
||||
private readPosition: number = 0;
|
||||
|
||||
public static from(buf: Buffer): GBufferReader {
|
||||
return new GBufferReader(buf);
|
||||
}
|
||||
|
||||
protected constructor(buf: Buffer) {
|
||||
this.buffer = buf;
|
||||
}
|
||||
|
||||
public get bytesLeft(): number {
|
||||
return Math.max(0, this.buffer.length - this.readPosition);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.readPosition = 0;
|
||||
}
|
||||
|
||||
public seek(idx: number): void {
|
||||
this.readPosition = Math.min(this.buffer.length, Math.max(0, idx));
|
||||
}
|
||||
|
||||
public readInt8(): number {
|
||||
let res = this.buffer.readInt8(this.readPosition);
|
||||
this.readPosition += 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readUInt8(): number {
|
||||
let res = this.buffer.readUInt8(this.readPosition);
|
||||
this.readPosition += 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readUInt16BE(): number {
|
||||
let res = this.buffer.readUInt16BE(this.readPosition);
|
||||
this.readPosition += 2;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readGUInt8(): number {
|
||||
let res = decodeBits(
|
||||
this.buffer.slice(this.readPosition, this.readPosition + 1),
|
||||
);
|
||||
// let res = this.buffer.readUInt8(this.readPosition) - 32;
|
||||
this.readPosition += 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readGUInt16(): number {
|
||||
let res = decodeBits(
|
||||
this.buffer.slice(this.readPosition, this.readPosition + 2),
|
||||
);
|
||||
this.readPosition += 2;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readGUInt24(): number {
|
||||
let res = decodeBits(
|
||||
this.buffer.slice(this.readPosition, this.readPosition + 3),
|
||||
);
|
||||
this.readPosition += 3;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readGUInt32(): number {
|
||||
let res = decodeBits(
|
||||
this.buffer.slice(this.readPosition, this.readPosition + 4),
|
||||
);
|
||||
this.readPosition += 4;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readGULong(): number {
|
||||
let res = decodeBits(
|
||||
this.buffer.slice(this.readPosition, this.readPosition + 5),
|
||||
);
|
||||
this.readPosition += 5;
|
||||
return res;
|
||||
}
|
||||
|
||||
public read(len: number): Buffer {
|
||||
const newReadPosition = Math.min(
|
||||
this.buffer.length,
|
||||
this.readPosition + len,
|
||||
);
|
||||
let res = this.buffer.slice(this.readPosition, newReadPosition);
|
||||
this.readPosition = newReadPosition;
|
||||
return res;
|
||||
}
|
||||
|
||||
public readChars(len: number): string {
|
||||
return this.read(len).toString('latin1');
|
||||
}
|
||||
|
||||
public readGBuffer(len?: number): GBufferReader {
|
||||
if (!len) {
|
||||
len = this.readGUInt8();
|
||||
}
|
||||
|
||||
return new GBufferReader(this.read(len));
|
||||
}
|
||||
|
||||
public readGString(): string {
|
||||
return this.readChars(this.readGUInt8());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically-allocated buffer, used for writing packet data
|
||||
*/
|
||||
export class GBufferWriter {
|
||||
private internalBuffer: Buffer;
|
||||
private writePosition: number;
|
||||
|
||||
constructor(capacity: number) {
|
||||
this.internalBuffer = Buffer.allocUnsafe(capacity);
|
||||
this.writePosition = 0;
|
||||
}
|
||||
|
||||
public static create(
|
||||
capacity: number = DEFAULT_BUFFER_SIZE,
|
||||
): GBufferWriter {
|
||||
return new GBufferWriter(capacity);
|
||||
}
|
||||
|
||||
public get buffer(): Buffer {
|
||||
return this.internalBuffer.slice(0, this.writePosition);
|
||||
}
|
||||
|
||||
public get capacity(): number {
|
||||
return this.internalBuffer.length;
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this.writePosition;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.writePosition = 0;
|
||||
}
|
||||
|
||||
public resize(minimum?: number): boolean {
|
||||
minimum = minimum || this.internalBuffer.length;
|
||||
if (minimum < this.internalBuffer.length) return false;
|
||||
|
||||
minimum *= 2;
|
||||
|
||||
let newbuf = Buffer.allocUnsafe(minimum);
|
||||
newbuf.set(this.internalBuffer);
|
||||
this.internalBuffer = newbuf;
|
||||
return true;
|
||||
}
|
||||
|
||||
public writeBuffer(buf: Buffer): void {
|
||||
const newSize = this.length + buf.length;
|
||||
if (newSize >= this.capacity) this.resize(newSize);
|
||||
|
||||
this.internalBuffer.set(buf, this.writePosition);
|
||||
this.writePosition = newSize;
|
||||
}
|
||||
|
||||
public writeChars(buf: string): void {
|
||||
const newSize = this.length + buf.length;
|
||||
if (newSize >= this.capacity) this.resize(newSize);
|
||||
|
||||
this.internalBuffer.write(buf, this.writePosition, 'latin1');
|
||||
this.writePosition = newSize;
|
||||
}
|
||||
|
||||
public writeUInt8(v: number): void {
|
||||
if (this.length + 1 >= this.capacity) this.resize();
|
||||
|
||||
this.internalBuffer.writeUInt8(v, this.writePosition);
|
||||
this.writePosition += 1;
|
||||
}
|
||||
|
||||
public writeGUInt8(v: number): void {
|
||||
if (this.length + 1 >= this.capacity) this.resize();
|
||||
|
||||
let b = [0];
|
||||
encodeBits(Math.min(v, 223), b);
|
||||
|
||||
for (let i = 0; i < b.length; i++)
|
||||
this.internalBuffer.writeUInt8(b[i], this.writePosition + i);
|
||||
this.writePosition += b.length;
|
||||
|
||||
// v = Math.min(v, 223);
|
||||
// this.internalBuffer.writeUInt8(v + 32, this.writePosition);
|
||||
// this.writePosition += 1;
|
||||
}
|
||||
|
||||
public writeGUInt16(v: number): void {
|
||||
if (this.length + 2 >= this.capacity) this.resize();
|
||||
|
||||
let b = [0, 0];
|
||||
encodeBits(Math.min(v, 28767), b);
|
||||
|
||||
for (let i = 0; i < b.length; i++)
|
||||
this.internalBuffer.writeUInt8(b[i], this.writePosition + i);
|
||||
this.writePosition += b.length;
|
||||
}
|
||||
|
||||
public writeGUInt24(v: number): void {
|
||||
if (this.length + 3 >= this.capacity) this.resize();
|
||||
|
||||
let b = [0, 0, 0];
|
||||
encodeBits(Math.min(v, 3682399), b);
|
||||
|
||||
for (let i = 0; i < b.length; i++)
|
||||
this.internalBuffer.writeUInt8(b[i], this.writePosition + i);
|
||||
this.writePosition += b.length;
|
||||
}
|
||||
|
||||
public writeGUInt32(v: number): void {
|
||||
if (this.length + 4 >= this.capacity) this.resize();
|
||||
|
||||
let b = [0, 0, 0, 0];
|
||||
encodeBits(Math.min(v, 471347295), b);
|
||||
|
||||
for (let i = 0; i < b.length; i++)
|
||||
this.internalBuffer.writeUInt8(b[i], this.writePosition + i);
|
||||
this.writePosition += b.length;
|
||||
}
|
||||
|
||||
public writeGULong(v: number): void {
|
||||
if (this.length + 5 >= this.capacity) this.resize();
|
||||
|
||||
let b = [0, 0, 0, 0, 0];
|
||||
encodeBits(Math.min(v, 0xffffffff), b);
|
||||
|
||||
for (let i = 0; i < b.length; i++)
|
||||
this.internalBuffer.writeUInt8(b[i], this.writePosition + i);
|
||||
this.writePosition += b.length;
|
||||
}
|
||||
|
||||
public writeGString(str: string) {
|
||||
this.writeGUInt8(str.length);
|
||||
this.writeChars(str);
|
||||
}
|
||||
}
|
||||
164
src/GProtocol.ts
Normal file
164
src/GProtocol.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import zlib = require('zlib');
|
||||
|
||||
// @ts-ignore
|
||||
import compressjs = require('compressjs');
|
||||
|
||||
export enum ProtocolGen {
|
||||
Gen1 = 0,
|
||||
Gen2 = 1,
|
||||
Gen3 = 2,
|
||||
Gen4 = 3,
|
||||
Gen5 = 4,
|
||||
}
|
||||
|
||||
enum CompressionType {
|
||||
NONE = 0x02,
|
||||
ZLIB = 0x04,
|
||||
BZ2 = 0x06,
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
Outgoing = 0,
|
||||
Incoming = 1,
|
||||
}
|
||||
|
||||
const start_iterator = [0, 0, 0x04a80b38, 0x4a80b38, 0x4a80b38, 0];
|
||||
|
||||
function getIteratorLimit(compressionType: CompressionType): number {
|
||||
return compressionType == CompressionType.NONE ? 0x0c : 0x04;
|
||||
}
|
||||
|
||||
export class GProtocol {
|
||||
key: number = 0;
|
||||
iterator: Uint32Array = new Uint32Array(2);
|
||||
gen: ProtocolGen = ProtocolGen.Gen1;
|
||||
|
||||
public static generateKey(): number {
|
||||
return Math.floor(Math.random() * 128);
|
||||
}
|
||||
|
||||
public static initialize(gen: ProtocolGen, key?: number): GProtocol {
|
||||
key = key || GProtocol.generateKey();
|
||||
return new GProtocol(gen, key);
|
||||
}
|
||||
|
||||
protected constructor(gen: ProtocolGen, key: number) {
|
||||
this.key = key;
|
||||
this.gen = gen;
|
||||
this.iterator[0] = this.iterator[1] = start_iterator[gen];
|
||||
}
|
||||
|
||||
apply(compressionType: CompressionType, dir: Direction, buf: Buffer) {
|
||||
let limit = getIteratorLimit(compressionType);
|
||||
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
if (i % 4 == 0) {
|
||||
if (limit <= 0) break;
|
||||
|
||||
limit -= 1;
|
||||
|
||||
this.iterator[dir] =
|
||||
Math.imul(this.iterator[dir], 0x8088405) + this.key;
|
||||
}
|
||||
|
||||
buf[i] ^= (this.iterator[dir] >> ((i % 4) * 8)) & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
decrypt(buf: Buffer): Buffer {
|
||||
switch (this.gen) {
|
||||
case ProtocolGen.Gen1:
|
||||
return buf;
|
||||
|
||||
case ProtocolGen.Gen2:
|
||||
return zlib.inflateSync(buf);
|
||||
|
||||
case ProtocolGen.Gen3: {
|
||||
buf = zlib.inflateSync(buf);
|
||||
|
||||
this.iterator[Direction.Incoming] *= 0x8088405;
|
||||
this.iterator[Direction.Incoming] += this.key;
|
||||
let removePos =
|
||||
(this.iterator[Direction.Incoming] & 0x0ffff) % buf.length;
|
||||
|
||||
let newBuf = Buffer.allocUnsafe(buf.length - 1);
|
||||
buf.copy(newBuf, 0, 0, removePos);
|
||||
buf.copy(newBuf, removePos + 1, removePos + 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
case ProtocolGen.Gen4:
|
||||
case ProtocolGen.Gen5: {
|
||||
let compressionType = CompressionType.BZ2;
|
||||
if (this.gen == ProtocolGen.Gen5)
|
||||
compressionType = buf.readUInt8();
|
||||
|
||||
this.apply(compressionType, Direction.Incoming, buf.slice(1));
|
||||
|
||||
switch (compressionType) {
|
||||
case CompressionType.NONE:
|
||||
buf = buf.slice(1);
|
||||
break;
|
||||
|
||||
case CompressionType.ZLIB:
|
||||
buf = zlib.inflateSync(buf.slice(1));
|
||||
break;
|
||||
|
||||
case CompressionType.BZ2:
|
||||
buf = Buffer.from(
|
||||
compressjs.Bzip2.decompressFile(buf.slice(1)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encrypt(buf: Buffer): Buffer {
|
||||
switch (this.gen) {
|
||||
case ProtocolGen.Gen1:
|
||||
return buf;
|
||||
|
||||
case ProtocolGen.Gen2:
|
||||
return zlib.deflateSync(buf);
|
||||
|
||||
case ProtocolGen.Gen3:
|
||||
buf = zlib.deflateSync(buf);
|
||||
this.iterator[Direction.Outgoing] *= 0x8088405;
|
||||
this.iterator[Direction.Outgoing] += this.key;
|
||||
let removePos =
|
||||
(this.iterator[Direction.Outgoing] & 0x0ffff) % buf.length;
|
||||
|
||||
let newBuf = Buffer.allocUnsafe(buf.length - 1);
|
||||
buf.copy(newBuf, 0, 0, removePos);
|
||||
buf.copy(newBuf, removePos + 1, removePos + 1);
|
||||
return buf;
|
||||
|
||||
case ProtocolGen.Gen4:
|
||||
case ProtocolGen.Gen5: {
|
||||
let compressionType = CompressionType.BZ2;
|
||||
|
||||
// Forcing zlib over bz2 due to the bz2 implementation being coded in
|
||||
// javascript rather than a native node module.
|
||||
if (this.gen == ProtocolGen.Gen5)
|
||||
compressionType = CompressionType.ZLIB;
|
||||
|
||||
switch (compressionType) {
|
||||
case CompressionType.ZLIB:
|
||||
buf = zlib.deflateSync(buf);
|
||||
break;
|
||||
|
||||
case CompressionType.BZ2:
|
||||
buf = compressjs.Bzip2.compressFile(buf);
|
||||
break;
|
||||
}
|
||||
|
||||
this.apply(compressionType, Direction.Outgoing, buf);
|
||||
|
||||
return Buffer.concat([Buffer.from([compressionType]), buf]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/GSocket.ts
Normal file
159
src/GSocket.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import net = require('net');
|
||||
import { GProtocol, ProtocolGen } from './GProtocol';
|
||||
import { PacketHandler, PacketTable } from './PacketTable';
|
||||
|
||||
export type ConnectHandler = () => void;
|
||||
export type DisconnectHandler = (err?: Error) => void;
|
||||
|
||||
export interface SocketConfigurartion {
|
||||
connectCallback?: ConnectHandler;
|
||||
disconnectCallback?: DisconnectHandler;
|
||||
packetTable?: PacketTable;
|
||||
}
|
||||
|
||||
export class GSocket {
|
||||
private buffer: Buffer;
|
||||
private enc: GProtocol | null = null;
|
||||
private sock: net.Socket | null = null;
|
||||
private packetHandler: PacketTable;
|
||||
private config: SocketConfigurartion;
|
||||
|
||||
public rawBytesAhead: number = 0;
|
||||
|
||||
protected constructor(cfg: SocketConfigurartion) {
|
||||
this.buffer = Buffer.allocUnsafe(0);
|
||||
this.config = cfg;
|
||||
this.packetHandler = cfg?.packetTable || new PacketTable();
|
||||
}
|
||||
|
||||
public static connect(
|
||||
host: string,
|
||||
port: number,
|
||||
cfg?: SocketConfigurartion,
|
||||
): GSocket {
|
||||
const newSock = new GSocket(cfg || {});
|
||||
newSock.setProtocol(ProtocolGen.Gen2);
|
||||
newSock.connect(host, port);
|
||||
return newSock;
|
||||
}
|
||||
|
||||
// Expose PacketHandler methods
|
||||
public on(id: number, callback: PacketHandler) {
|
||||
return this.packetHandler.on(id, callback);
|
||||
}
|
||||
|
||||
private connect(host: string, port: number) {
|
||||
this.sock = new net.Socket();
|
||||
this.sock.setTimeout(10000);
|
||||
|
||||
this.sock.connect(port, host, this.config.connectCallback);
|
||||
|
||||
this.sock.on('data', (data: Buffer) => {
|
||||
this.buffer = Buffer.concat([this.buffer, data]);
|
||||
|
||||
while (this.buffer.length > 0) {
|
||||
const readLen = this.buffer.readUInt16BE();
|
||||
if (this.buffer.length < readLen + 2) break;
|
||||
|
||||
this.processData(this.buffer.slice(2, 2 + readLen));
|
||||
this.buffer = this.buffer.slice(2 + readLen);
|
||||
}
|
||||
});
|
||||
|
||||
this.sock.on('close', () => {
|
||||
console.log(`[SOCKET CLOSE] ${host}:${port}`);
|
||||
|
||||
this.config.disconnectCallback?.();
|
||||
});
|
||||
|
||||
this.sock.on('error', (error) => {
|
||||
console.log(`[SOCKET ERROR] ${host}:${port}`);
|
||||
|
||||
this.disconnect();
|
||||
});
|
||||
|
||||
this.sock.on('timeout', () => {
|
||||
console.log(`[SOCKET TIMED OUT] ${host}:${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.sock) {
|
||||
this.sock.destroy();
|
||||
this.sock = null;
|
||||
}
|
||||
}
|
||||
|
||||
public setProtocol(protocol: ProtocolGen, key?: number) {
|
||||
this.enc = GProtocol.initialize(protocol, key);
|
||||
}
|
||||
|
||||
public sendData(buf: Buffer) {
|
||||
if (this.enc) buf = this.enc.encrypt(buf);
|
||||
|
||||
let nb = Buffer.allocUnsafe(buf.length + 2);
|
||||
buf.copy(nb, 2);
|
||||
nb.writeUInt16BE(buf.length, 0);
|
||||
|
||||
this.sock?.write(nb);
|
||||
}
|
||||
|
||||
public sendPacket(id: number, buf?: Buffer) {
|
||||
const buffers = [Buffer.from([id + 32])];
|
||||
|
||||
if (buf) buffers.push(Buffer.from(buf));
|
||||
|
||||
// Append new line to packets
|
||||
if (!buf || buf[buf.length - 1] != 0xa)
|
||||
buffers.push(Buffer.from([0xa]));
|
||||
|
||||
return Buffer.concat(buffers);
|
||||
}
|
||||
|
||||
private processData(buf: Buffer) {
|
||||
if (this.enc) buf = this.enc.decrypt(buf);
|
||||
|
||||
let offset = 0;
|
||||
while (offset < buf.length) {
|
||||
let idx;
|
||||
|
||||
// Read data from buffer, terminating at '\n' or rawBytesAhead if defined
|
||||
if (this.rawBytesAhead > 0) {
|
||||
if (this.rawBytesAhead > buf.length - offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
idx = offset + this.rawBytesAhead;
|
||||
} else {
|
||||
idx = buf.indexOf('\n', offset);
|
||||
if (idx === -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy data into a new buffer
|
||||
let b = Buffer.allocUnsafe(idx - offset);
|
||||
buf.copy(b, 0, offset, idx);
|
||||
|
||||
if (this.rawBytesAhead > 0) {
|
||||
// Reset raw
|
||||
this.rawBytesAhead = 0;
|
||||
offset = idx;
|
||||
} else {
|
||||
// Skip passed the newline
|
||||
offset = idx + 1;
|
||||
}
|
||||
|
||||
// Process the packet
|
||||
let packetId = b.readUInt8();
|
||||
this.processPacket(packetId - 32, b.slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
private processPacket(id: number, buf: Buffer) {
|
||||
let handlers = this.packetHandler.getCallbacks(id);
|
||||
for (let handle of handlers) {
|
||||
handle(id, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Logger.ts
Normal file
23
src/Logger.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import chalk from 'chalk';
|
||||
|
||||
export class Logger {
|
||||
public static log(...args: unknown[]): void {
|
||||
console.log(chalk.blue('[LOG]'), ...args);
|
||||
}
|
||||
|
||||
public static error(...args: unknown[]): void {
|
||||
console.error(chalk.red('[ERROR]'), ...args);
|
||||
}
|
||||
|
||||
public static warn(...args: unknown[]): void {
|
||||
console.warn(chalk.yellow('[WARN]'), ...args);
|
||||
}
|
||||
|
||||
public static info(...args: unknown[]): void {
|
||||
console.info(chalk.green('[INFO]'), ...args);
|
||||
}
|
||||
|
||||
public static debug(...args: unknown[]): void {
|
||||
console.debug(chalk.cyan('[DEBUG]'), ...args);
|
||||
}
|
||||
}
|
||||
476
src/NpcControl.ts
Normal file
476
src/NpcControl.ts
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
import { GBufferReader, GBufferWriter } from './GBuffer';
|
||||
import { ProtocolGen } from './GProtocol';
|
||||
import { GSocket } from './GSocket';
|
||||
import { PacketTable } from './PacketTable';
|
||||
import { PromiseManager } from './PromiseManager';
|
||||
import { gtokenize, guntokenize } from './utils';
|
||||
import { NPC, NPCManager, NPCPropID } from './misc/npcs';
|
||||
import { NCIncomingPacket, NCOutgoingPacket } from './misc/packet';
|
||||
import { NCEvents, NCInterface, ServerlistConfig } from './types';
|
||||
|
||||
enum UriConstants {
|
||||
NpcPrefix = 'npcserver://npcs/',
|
||||
ScriptPrefix = 'npcserver://scripts/',
|
||||
WeaponPrefix = 'npcserver://weapons/',
|
||||
WeaponList = 'npcserver://weapons',
|
||||
LevelList = 'npcserver://levellist',
|
||||
}
|
||||
|
||||
enum ErrorMsg {
|
||||
NotFound = 'Resource not found',
|
||||
}
|
||||
|
||||
interface NpcControlConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export class NPCControl implements NCInterface {
|
||||
private sock?: GSocket;
|
||||
private readonly packetTable: PacketTable;
|
||||
private readonly eventHandler: NCEvents;
|
||||
|
||||
private promiseMngr: PromiseManager = new PromiseManager();
|
||||
private npcMngr: NPCManager = new NPCManager();
|
||||
private classList: Set<string> = new Set<string>();
|
||||
|
||||
public get classes(): Set<string> {
|
||||
return this.classList;
|
||||
}
|
||||
|
||||
public get npcs(): NPC[] {
|
||||
return this.npcMngr.npcs;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly config: ServerlistConfig,
|
||||
ncConfig: NpcControlConfig,
|
||||
eventHandler: NCEvents,
|
||||
) {
|
||||
this.eventHandler = eventHandler;
|
||||
this.packetTable = this.initializeHandlers();
|
||||
this.connect(ncConfig.host, ncConfig.port);
|
||||
}
|
||||
|
||||
public connect(host: string, port: number): boolean {
|
||||
if (this.sock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.sock = GSocket.connect(host, port, {
|
||||
connectCallback: () => this.onConnect(),
|
||||
disconnectCallback: () => this.onDisconnect(),
|
||||
packetTable: this.packetTable,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this.sock?.disconnect();
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
private onConnect(): void {
|
||||
console.log('[NC] Connected!');
|
||||
|
||||
if (!this.sock) {
|
||||
console.log('[NC] no sock?');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send login packet
|
||||
let nb = GBufferWriter.create();
|
||||
nb.writeChars('NCL21075');
|
||||
nb.writeGString(this.config.account);
|
||||
nb.writeGString(this.config.password);
|
||||
this.sock.sendData(this.sock.sendPacket(3, nb.buffer));
|
||||
|
||||
this.sock.setProtocol(ProtocolGen.Gen3, 0);
|
||||
|
||||
// The only proof that you passed verification, is the fact that you are
|
||||
// still connected to the server.
|
||||
this.eventHandler.onNCConnected?.();
|
||||
}
|
||||
|
||||
private onDisconnect(): void {
|
||||
console.log('[NC] Disconnected!');
|
||||
|
||||
if (!this.sock) {
|
||||
console.log('[NC] no sock?');
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventHandler.onNCDisconnected?.();
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
requestLevelList(): Promise<string> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_LEVELLISTGET),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.LevelList);
|
||||
}
|
||||
|
||||
deleteWeapon(name: string): void {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_WEAPONDELETE,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
requestWeaponList(): Promise<Set<string>> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_WEAPONLISTGET),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.WeaponList);
|
||||
}
|
||||
|
||||
requestWeapon(name: string): Promise<[string, string]> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_WEAPONGET,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.WeaponPrefix + name);
|
||||
}
|
||||
|
||||
setWeaponScript(name: string, image: string, script: string): void {
|
||||
// Weapons are sent by replacing newlines with \xa7 character
|
||||
script = script.replace(/\n/g, '§');
|
||||
script = script.replace(/\r/g, '');
|
||||
|
||||
const writer = GBufferWriter.create(
|
||||
1 + name.length + 1 + image.length + script.length,
|
||||
);
|
||||
writer.writeGString(name);
|
||||
writer.writeGString(image);
|
||||
writer.writeChars(script);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_WEAPONADD,
|
||||
writer.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
deleteNpc(name: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
requestNpcAttributes(name: string): Promise<string> {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_NPCGET, nb.buffer),
|
||||
);
|
||||
return this.promiseMngr.createPromise(
|
||||
UriConstants.NpcPrefix + name + '.attrs',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(ErrorMsg.NotFound);
|
||||
}
|
||||
|
||||
requestNpcFlags(name: string): Promise<string> {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCFLAGSGET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(
|
||||
UriConstants.NpcPrefix + name + '.flags',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(ErrorMsg.NotFound);
|
||||
}
|
||||
|
||||
requestNpcScript(name: string): Promise<string> {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCSCRIPTGET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(
|
||||
UriConstants.NpcPrefix + name + '.script',
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(ErrorMsg.NotFound);
|
||||
}
|
||||
|
||||
setNpcFlags(name: string, script: string): void {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
nb.writeChars(gtokenize(script));
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCFLAGSSET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setNpcScript(name: string, script: string): void {
|
||||
const npcObject = this.npcMngr.findNPC(name);
|
||||
if (npcObject) {
|
||||
const nb = GBufferWriter.create(3);
|
||||
nb.writeGUInt24(npcObject.id);
|
||||
nb.writeChars(gtokenize(script));
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_NPCSCRIPTSET,
|
||||
nb.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deleteClass(name: string): void {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_CLASSDELETE,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
requestClass(name: string): Promise<string> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
NCOutgoingPacket.PLI_NC_CLASSEDIT,
|
||||
Buffer.from(name),
|
||||
),
|
||||
);
|
||||
return this.promiseMngr.createPromise(UriConstants.ScriptPrefix + name);
|
||||
}
|
||||
|
||||
setClassScript(name: string, script: string): void {
|
||||
const nb = GBufferWriter.create();
|
||||
nb.writeGString(name);
|
||||
nb.writeChars(gtokenize(script));
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(NCOutgoingPacket.PLI_NC_CLASSADD, nb.buffer),
|
||||
);
|
||||
}
|
||||
|
||||
private initializeHandlers(): PacketTable {
|
||||
const packetTable = new PacketTable();
|
||||
|
||||
packetTable.setDefault((id: number, packet: Buffer): void => {
|
||||
if (id !== 42) {
|
||||
console.log(
|
||||
`[NC] Unhandled Packet (${id}): ${packet
|
||||
.toString()
|
||||
.replace(/\r/g, '')}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NEWWORLDTIME,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const serverTime = reader.readGUInt32();
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_RCCHAT,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const msg = packet.toString();
|
||||
this.eventHandler.onNCChat?.(msg);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_LEVELLIST,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const levelList = guntokenize(packet.toString());
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.LevelList,
|
||||
levelList,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCATTRIBUTES,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const text = guntokenize(packet.toString());
|
||||
const name = text
|
||||
.substring(
|
||||
'Variable dump from npc '.length + 1,
|
||||
text.indexOf('\n'),
|
||||
)
|
||||
.trimEnd();
|
||||
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.NpcPrefix + name + '.attrs',
|
||||
text,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCADD,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
|
||||
const npcId = reader.readGUInt24();
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
npcObj.setProps(reader);
|
||||
|
||||
this.eventHandler.onNpcAdded?.(
|
||||
npcObj.props[NPCPropID.NPCPROP_NAME] as string,
|
||||
);
|
||||
// console.log(`NPC Added: ${npcObj.props[NPCPropID.NPCPROP_NAME]}`);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCDELETE,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
|
||||
const npcId = reader.readGUInt24();
|
||||
// this.npcMngr.deleteNpc(npcId);
|
||||
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
const npcName = npcObj.props[NPCPropID.NPCPROP_NAME] as string;
|
||||
|
||||
if (this.npcMngr.deleteNpc(npcId)) {
|
||||
this.eventHandler.onNpcDeleted?.(npcName);
|
||||
console.log(`Delete npc ${npcName}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCSCRIPT,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const npcId = reader.readGUInt24();
|
||||
const text = guntokenize(reader.readChars(reader.bytesLeft));
|
||||
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
if (npcObj) {
|
||||
const npcName = npcObj.props[
|
||||
NPCPropID.NPCPROP_NAME
|
||||
] as string;
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.NpcPrefix + npcName + '.script',
|
||||
text,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_NPCFLAGS,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const npcId = reader.readGUInt24();
|
||||
const text = guntokenize(reader.readChars(reader.bytesLeft));
|
||||
|
||||
const npcObj = this.npcMngr.getNpc(npcId);
|
||||
if (npcObj) {
|
||||
const npcName = npcObj.props[
|
||||
NPCPropID.NPCPROP_NAME
|
||||
] as string;
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.NpcPrefix + npcName + '.flags',
|
||||
text,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_CLASSGET,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const name = reader.readGString();
|
||||
const script = guntokenize(reader.readChars(reader.bytesLeft));
|
||||
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.ScriptPrefix + name,
|
||||
script,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_CLASSADD,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const className = packet.toString();
|
||||
this.classList.add(className);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_CLASSDELETE,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const className = packet.toString();
|
||||
this.classList.delete(className);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_WEAPONGET,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const reader = GBufferReader.from(packet);
|
||||
const name = reader.readGString();
|
||||
const image = reader.readGString();
|
||||
|
||||
let script = reader.readChars(reader.bytesLeft);
|
||||
script = script.replace(/\xa7/g, '\n');
|
||||
|
||||
this.promiseMngr.resolvePromise<[string, string]>(
|
||||
UriConstants.WeaponPrefix + name,
|
||||
[image, script],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
packetTable.on(
|
||||
NCIncomingPacket.PLO_NC_WEAPONLISTGET,
|
||||
(id: number, packet: Buffer): void => {
|
||||
const weaponList: Set<string> = new Set();
|
||||
|
||||
const reader = GBufferReader.from(packet);
|
||||
while (reader.bytesLeft) {
|
||||
weaponList.add(reader.readGString());
|
||||
}
|
||||
|
||||
this.promiseMngr.resolvePromise(
|
||||
UriConstants.WeaponList,
|
||||
weaponList,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return packetTable;
|
||||
}
|
||||
}
|
||||
32
src/PacketTable.ts
Normal file
32
src/PacketTable.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export type PacketHandler = (id: number, packet: Buffer) => void;
|
||||
|
||||
export class PacketTable {
|
||||
static readonly ALL_HANDLER = -1;
|
||||
static readonly DEFAULT_HANDLER = -2;
|
||||
|
||||
private handlers: PacketHandler[][] = [];
|
||||
|
||||
public getCallbacks(id: number): PacketHandler[] {
|
||||
let callbacks = (this.handlers[PacketTable.ALL_HANDLER] || []).concat(
|
||||
this.handlers[id] || [],
|
||||
);
|
||||
if (callbacks.length == 0)
|
||||
callbacks = this.handlers[PacketTable.DEFAULT_HANDLER] || callbacks;
|
||||
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
public setCatchAll(callback: PacketHandler) {
|
||||
this.on(-1, callback);
|
||||
}
|
||||
|
||||
public setDefault(callback: PacketHandler) {
|
||||
this.on(-2, callback);
|
||||
}
|
||||
|
||||
public on(id: number, callback: PacketHandler) {
|
||||
if (!(id in this.handlers)) this.handlers[id] = [];
|
||||
|
||||
this.handlers[id].push(callback);
|
||||
}
|
||||
}
|
||||
44
src/PromiseManager.ts
Normal file
44
src/PromiseManager.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const timedOutMessage: string = 'Timed out';
|
||||
|
||||
export interface PromiseData<T> {
|
||||
resolve(val: T | PromiseLike<T>): void;
|
||||
reject(reason?: any): void;
|
||||
}
|
||||
|
||||
export class PromiseManager {
|
||||
private promiseData: { [uri: string]: PromiseData<any> } = {};
|
||||
|
||||
createPromise<Type>(uri: string, timeout: number = 10): Promise<Type> {
|
||||
return new Promise<Type>((resolve, reject) => {
|
||||
this.promiseData[uri] = {
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
};
|
||||
|
||||
if (timeout) {
|
||||
setTimeout(() => reject(timedOutMessage), timeout * 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolvePromise<Type>(uri: string, data: Type): void {
|
||||
if (uri in this.promiseData) {
|
||||
this.promiseData[uri].resolve(data);
|
||||
delete this.promiseData[uri];
|
||||
}
|
||||
}
|
||||
|
||||
rejectPromise<Type>(uri: string, data: Type): void {
|
||||
if (uri in this.promiseData) {
|
||||
this.promiseData[uri].reject(data);
|
||||
delete this.promiseData[uri];
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
for (const val of Object.values(this.promiseData)) {
|
||||
val.reject();
|
||||
}
|
||||
this.promiseData = {};
|
||||
}
|
||||
}
|
||||
274
src/RemoteControl.ts
Normal file
274
src/RemoteControl.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import { networkInterfaces } from 'os';
|
||||
import { FileBrowser, RCFileBrowser } from './FileBrowser';
|
||||
import { GBufferWriter } from './GBuffer';
|
||||
import { GProtocol, ProtocolGen } from './GProtocol';
|
||||
import { GSocket } from './GSocket';
|
||||
import { Logger } from './Logger';
|
||||
import { NPCControl } from './NpcControl';
|
||||
import { PacketTable } from './PacketTable';
|
||||
import { PromiseManager } from './PromiseManager';
|
||||
import {
|
||||
NCEvents,
|
||||
NCInterface,
|
||||
RCEvents,
|
||||
RCInterface,
|
||||
ServerEntry,
|
||||
ServerlistConfig,
|
||||
} from './types';
|
||||
import { hashCode } from './utils';
|
||||
import { PlayerProperties, RCOutgoingPacket } from './misc/packet';
|
||||
|
||||
enum UriConstants {
|
||||
FolderConfig = 'gserver://config/folderconfig',
|
||||
ServerFlags = 'gserver://config/serverflags',
|
||||
ServerOptions = 'gserver://config/serveroptions',
|
||||
}
|
||||
|
||||
interface RemoteControlEvents extends NCEvents, RCEvents {}
|
||||
|
||||
export class RemoteControl implements RCInterface {
|
||||
private readonly config: ServerlistConfig;
|
||||
public readonly server: ServerEntry;
|
||||
private readonly packetTable: PacketTable;
|
||||
private readonly fileBrowser: RCFileBrowser;
|
||||
private readonly promiseManager: PromiseManager = new PromiseManager();
|
||||
private readonly eventHandler: RemoteControlEvents;
|
||||
|
||||
private sock?: GSocket;
|
||||
private npcControl?: NPCControl;
|
||||
private disconnectMsg?: string;
|
||||
private maxUploadSize = 0;
|
||||
|
||||
public get socket(): GSocket | undefined {
|
||||
return this.sock;
|
||||
}
|
||||
|
||||
public get FileBrowser(): FileBrowser {
|
||||
return this.fileBrowser;
|
||||
}
|
||||
|
||||
public get NpcControl(): NCInterface | undefined {
|
||||
return this.npcControl;
|
||||
}
|
||||
|
||||
public get maxUploadFileSize(): number {
|
||||
return this.maxUploadSize;
|
||||
}
|
||||
|
||||
constructor(
|
||||
config: ServerlistConfig,
|
||||
server: ServerEntry,
|
||||
eventHandler: RemoteControlEvents,
|
||||
) {
|
||||
this.config = config;
|
||||
this.server = server;
|
||||
this.eventHandler = eventHandler;
|
||||
this.fileBrowser = new RCFileBrowser(this);
|
||||
|
||||
this.packetTable = this.initializeHandlers();
|
||||
this.connect(server.ip, server.port);
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return !!this.sock;
|
||||
}
|
||||
|
||||
public connect(host: string, port: number): boolean {
|
||||
if (this.sock) return false;
|
||||
|
||||
this.sock = GSocket.connect(host, port, {
|
||||
connectCallback: () => this.onConnect(),
|
||||
disconnectCallback: (err?: Error) => this.onDisconnect(err),
|
||||
packetTable: this.packetTable,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private onConnect(): void {
|
||||
Logger.log(
|
||||
`RC Connected to ${this.server.ip}:${this.server.port} (${this.server.name})`,
|
||||
);
|
||||
|
||||
if (!this.sock) return;
|
||||
|
||||
this.sendLoginPacket();
|
||||
}
|
||||
|
||||
private onDisconnect(err?: Error): void {
|
||||
this.sock = undefined;
|
||||
|
||||
if (this.npcControl) {
|
||||
this.npcControl.disconnect();
|
||||
this.npcControl = undefined;
|
||||
}
|
||||
|
||||
this.eventHandler.onRCDisconnected?.(
|
||||
this,
|
||||
this.disconnectMsg || err?.message,
|
||||
);
|
||||
|
||||
Logger.warn('RC Disconnected');
|
||||
}
|
||||
|
||||
private requestNpcServer(): void {
|
||||
const writer = GBufferWriter.create(2 + 'location'.length);
|
||||
writer.writeGUInt16(2); // 2 = NpcServer
|
||||
writer.writeChars('location');
|
||||
this.sock?.sendData(
|
||||
this.sock?.sendPacket(RCOutgoingPacket.PLI_RC_FILEBROWSER_START),
|
||||
);
|
||||
}
|
||||
|
||||
private connectToNpcServer(host: string, port: number): NPCControl | null {
|
||||
if (this.npcControl) {
|
||||
this.npcControl.disconnect();
|
||||
this.npcControl = undefined;
|
||||
}
|
||||
|
||||
this.npcControl = new NPCControl(
|
||||
this.config,
|
||||
{
|
||||
host: host,
|
||||
port: port,
|
||||
},
|
||||
this.eventHandler,
|
||||
);
|
||||
|
||||
return this.npcControl;
|
||||
}
|
||||
|
||||
requestFolderConfig(): Promise<string> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(RCOutgoingPacket.PLI_RC_FOLDERCONFIGGET),
|
||||
);
|
||||
return this.promiseManager.createPromise(UriConstants.FolderConfig);
|
||||
}
|
||||
|
||||
requestServerFlags(): Promise<string> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(RCOutgoingPacket.PLI_RC_SERVERFLAGSGET),
|
||||
);
|
||||
return this.promiseManager.createPromise(UriConstants.ServerFlags);
|
||||
}
|
||||
|
||||
requestServerOptions(): Promise<string> {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(RCOutgoingPacket.PLI_RC_SERVEROPTIONSGET),
|
||||
);
|
||||
return this.promiseManager.createPromise(UriConstants.ServerOptions);
|
||||
}
|
||||
|
||||
sendRCChat(msg: string): void {
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(RCOutgoingPacket.PLI_RCCHAT, Buffer.from(msg)),
|
||||
);
|
||||
}
|
||||
|
||||
setNickName(name: string): void {
|
||||
const writer = GBufferWriter.create(2 + name.length);
|
||||
writer.writeGUInt8(PlayerProperties.PLPROP_NICKNAME);
|
||||
writer.writeGString(name);
|
||||
this.sock?.sendData(
|
||||
this.sock.sendPacket(
|
||||
RCOutgoingPacket.PLI_PLAYERPROPS,
|
||||
writer.buffer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
initializeHandlers(): PacketTable {
|
||||
const packetTable = new PacketTable();
|
||||
|
||||
packetTable.setDefault((id: number, packet: Buffer): void => {
|
||||
// Logger.error(
|
||||
// `[RC] Unhandled Packet (${id}): ${packet
|
||||
// .toString()
|
||||
// .replace(/\r/g, '')}`,
|
||||
// );
|
||||
});
|
||||
|
||||
// RC Messages
|
||||
packetTable.on(74, (id: number, packet: Buffer): void => {
|
||||
// console.log(`[RC] ${packet.toString()}`);
|
||||
this.eventHandler.onRCEventMsg?.(packet.toString());
|
||||
});
|
||||
|
||||
// This is just a heartbeat packet, we can ignore it
|
||||
packetTable.on(42, (): void => {});
|
||||
|
||||
return packetTable;
|
||||
}
|
||||
|
||||
private sendLoginPacket(): void {
|
||||
if (!this.sock) return;
|
||||
|
||||
const key = GProtocol.generateKey();
|
||||
const writer = GBufferWriter.create();
|
||||
|
||||
writer.writeGUInt8(key);
|
||||
writer.writeChars('GSERV025');
|
||||
writer.writeGString(this.config.account);
|
||||
writer.writeGString(this.config.password);
|
||||
writer.writeChars('mac,');
|
||||
|
||||
const getComputerCode = (computerHash: number) => {
|
||||
const hexCode = [
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
];
|
||||
|
||||
let stringBuilder = '';
|
||||
for (let i = 0; i < 32; i++) {
|
||||
stringBuilder += hexCode[Math.abs(computerHash % 16)];
|
||||
computerHash *= 31;
|
||||
}
|
||||
|
||||
return stringBuilder;
|
||||
};
|
||||
const getComputerHash = (): number => {
|
||||
const nets = networkInterfaces();
|
||||
const interfaces = Object.values(nets).flat().filter(Boolean);
|
||||
const macAddress = interfaces.find(
|
||||
(iface) => iface && iface.mac !== '00:00:00:00:00:00',
|
||||
)?.mac;
|
||||
|
||||
return macAddress
|
||||
? hashCode(macAddress.split(':').join(''))
|
||||
: Math.random() * 100000;
|
||||
};
|
||||
|
||||
let computerCodeString;
|
||||
const computerHash = getComputerHash();
|
||||
computerCodeString = getComputerCode(
|
||||
computerHash + hashCode(this.config.account),
|
||||
);
|
||||
|
||||
writer.writeChars(computerCodeString);
|
||||
writer.writeChars(',');
|
||||
writer.writeChars(computerCodeString);
|
||||
writer.writeChars(',');
|
||||
writer.writeChars('""');
|
||||
|
||||
this.sock.sendData(this.sock.sendPacket(6, writer.buffer));
|
||||
this.sock.setProtocol(ProtocolGen.Gen5, key);
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this.sock?.disconnect();
|
||||
}
|
||||
}
|
||||
61
src/index.ts
Normal file
61
src/index.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { config } from 'dotenv';
|
||||
import { ServerEntry, ServerlistConfig } from './types';
|
||||
import { Serverlister } from './serverLister';
|
||||
import { Logger } from './Logger';
|
||||
import { RemoteControl } from './RemoteControl';
|
||||
import axios from 'axios';
|
||||
|
||||
const env = config({ path: './.env' });
|
||||
|
||||
async function main() {
|
||||
const config: ServerlistConfig = {
|
||||
host: 'listserver.graalonline.com',
|
||||
port: 14922,
|
||||
account: env.parsed?.ACCOUNT || '',
|
||||
password: env.parsed?.PASSWORD || '',
|
||||
nickname: 'Ruan',
|
||||
};
|
||||
|
||||
try {
|
||||
const servers = await Serverlister.request(config);
|
||||
const zodiacDev = servers.find((s: ServerEntry) => s.name === 'Zodiac');
|
||||
|
||||
if (!zodiacDev) {
|
||||
Logger.error('Zodiac Dev server not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
new RemoteControl(config, zodiacDev, {
|
||||
onRCConnected: (instance) => {
|
||||
Logger.debug('Connected to RC');
|
||||
},
|
||||
onRCDisconnected: (instance, text) => {
|
||||
Logger.warn('Disconnected from RC:', text);
|
||||
},
|
||||
onRCChat: (text) => {
|
||||
Logger.info('RC Chat:', text);
|
||||
},
|
||||
onRCEventMsg: (text) => {
|
||||
if (!env.parsed?.RC_LOG_DISCORD_WEBHOOK) return;
|
||||
try {
|
||||
axios
|
||||
.post(env.parsed?.RC_LOG_DISCORD_WEBHOOK, {
|
||||
content: '`' + text + '`',
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error(
|
||||
'Error sending message to Discord:',
|
||||
error,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error('Error:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
78
src/misc/npcs.ts
Normal file
78
src/misc/npcs.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { GBufferReader } from '../GBuffer';
|
||||
|
||||
type PropData = number | string;
|
||||
|
||||
export enum NPCPropID {
|
||||
NPCPROP_ID = 17,
|
||||
NPCPROP_SCRIPTER = 49,
|
||||
NPCPROP_NAME = 50,
|
||||
NPCPROP_TYPE = 51,
|
||||
NPCPROP_CURLEVEL = 52,
|
||||
}
|
||||
|
||||
export class NPCManager {
|
||||
private readonly npcList: { [key: number]: NPC } = {};
|
||||
|
||||
public get npcs(): NPC[] {
|
||||
return Object.values(this.npcList); //.filter((v) => NPCPropID.NPCPROP_NAME in v.props);
|
||||
}
|
||||
|
||||
public deleteNpc(id: number): boolean {
|
||||
if (id in this.npcList) {
|
||||
delete this.npcList[id];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getNpc(id: number, create: boolean = true): NPC {
|
||||
if (create && !(id in this.npcList)) {
|
||||
this.npcList[id] = new NPC(id);
|
||||
}
|
||||
|
||||
return this.npcList[id] || undefined;
|
||||
}
|
||||
|
||||
public findNPC(name: string): NPC | undefined {
|
||||
for (const [k, v] of Object.entries(this.npcList)) {
|
||||
if (v.getProp(NPCPropID.NPCPROP_NAME) === name) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NPC {
|
||||
props: { [key: number]: PropData } = {};
|
||||
|
||||
constructor(public readonly id: number) {
|
||||
this.props[NPCPropID.NPCPROP_ID] = id;
|
||||
}
|
||||
|
||||
getProp(id: number): PropData {
|
||||
return this.props[id];
|
||||
}
|
||||
|
||||
setProps(reader: GBufferReader) {
|
||||
while (reader.bytesLeft) {
|
||||
const propId: NPCPropID = reader.readGUInt8();
|
||||
switch (propId) {
|
||||
case NPCPropID.NPCPROP_ID:
|
||||
this.props[propId] = reader.readGUInt16();
|
||||
break;
|
||||
|
||||
case NPCPropID.NPCPROP_SCRIPTER:
|
||||
case NPCPropID.NPCPROP_NAME:
|
||||
case NPCPropID.NPCPROP_TYPE:
|
||||
case NPCPropID.NPCPROP_CURLEVEL:
|
||||
this.props[propId] = reader.readGString();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled NPC Property: ${NPCPropID}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/misc/packet.ts
Normal file
139
src/misc/packet.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
export enum RCIncomingPacket {
|
||||
PLO_DISCMESSAGE = 16,
|
||||
PLO_SIGNATURE = 25,
|
||||
PLO_FILESENDFAILED = 30,
|
||||
PLO_NEWWORLDTIME = 42,
|
||||
PLO_RC_ACCOUNTADD = 50, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_RC_ACCOUNTSTATUS = 51, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_RC_ACCOUNTNAME = 52, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_RC_ACCOUNTDEL = 53, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_RC_ACCOUNTPROPS = 54, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_ADDPLAYER = 55, // Unhandled by 5.07+. -Codr
|
||||
PLO_DELPLAYER = 56,
|
||||
PLO_RC_ACCOUNTPROPSGET = 57, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_RC_ACCOUNTCHANGE = 58, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_RC_PLAYERPROPSCHANGE = 59, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_UNKNOWN60 = 60, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_SERVERFLAGSGET = 61, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_PLAYERRIGHTSGET = 62, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_PLAYERCOMMENTSGET = 63, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_PLAYERBANGET = 64, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_FILEBROWSER_DIRLIST = 65, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_FILEBROWSER_DIR = 66, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_FILEBROWSER_MESSAGE = 67,
|
||||
PLO_LARGEFILESTART = 68,
|
||||
PLO_LARGEFILEEND = 69,
|
||||
PLO_RC_ACCOUNTLISTGET = 70, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_PLAYERPROPS = 71, // Deprecated. Codr note: Unhandled by 6.037.
|
||||
PLO_RC_PLAYERPROPSGET = 72, // Unhandled by 5.07+. -Codr
|
||||
PLO_RC_ACCOUNTGET = 73,
|
||||
PLO_RCCHAT = 74,
|
||||
PLO_RC_SERVEROPTIONSGET = 76,
|
||||
PLO_RC_FOLDERCONFIGGET = 77,
|
||||
PLO_NPCSERVERADDR = 79,
|
||||
PLO_LARGEFILESIZE = 84,
|
||||
PLO_RAWDATA = 100,
|
||||
PLO_FILE = 102,
|
||||
PLO_RC_MAXUPLOADFILESIZE = 103,
|
||||
}
|
||||
|
||||
export enum RCOutgoingPacket {
|
||||
PLI_PLAYERPROPS = 2,
|
||||
PLI_RAWDATA = 50,
|
||||
PLI_RC_SERVEROPTIONSGET = 51,
|
||||
PLI_RC_SERVEROPTIONSSET = 52,
|
||||
PLI_RC_FOLDERCONFIGGET = 53,
|
||||
PLI_RC_FOLDERCONFIGSET = 54,
|
||||
PLI_RC_RESPAWNSET = 55,
|
||||
PLI_RC_HORSELIFESET = 56,
|
||||
PLI_RC_APINCREMENTSET = 57,
|
||||
PLI_RC_BADDYRESPAWNSET = 58,
|
||||
PLI_RC_PLAYERPROPSGET = 59,
|
||||
PLI_RC_PLAYERPROPSSET = 60,
|
||||
PLI_RC_DISCONNECTPLAYER = 61,
|
||||
PLI_RC_UPDATELEVELS = 62,
|
||||
PLI_RC_ADMINMESSAGE = 63,
|
||||
PLI_RC_PRIVADMINMESSAGE = 64,
|
||||
PLI_RC_LISTRCS = 65,
|
||||
PLI_RC_DISCONNECTRC = 66,
|
||||
PLI_RC_APPLYREASON = 67,
|
||||
PLI_RC_SERVERFLAGSGET = 68,
|
||||
PLI_RC_SERVERFLAGSSET = 69,
|
||||
PLI_RC_ACCOUNTADD = 70,
|
||||
PLI_RC_ACCOUNTDEL = 71,
|
||||
PLI_RC_ACCOUNTLISTGET = 72,
|
||||
PLI_RC_PLAYERPROPSGET2 = 73,
|
||||
PLI_RC_PLAYERPROPSGET3 = 74,
|
||||
PLI_RC_PLAYERPROPSRESET = 75,
|
||||
PLI_RC_PLAYERPROPSSET2 = 76,
|
||||
PLI_RC_ACCOUNTGET = 77,
|
||||
PLI_RC_ACCOUNTSET = 78,
|
||||
PLI_RCCHAT = 79,
|
||||
PLI_RC_WARPPLAYER = 82,
|
||||
PLI_RC_PLAYERRIGHTSGET = 83,
|
||||
PLI_RC_PLAYERRIGHTSSET = 84,
|
||||
PLI_RC_PLAYERCOMMENTSGET = 85,
|
||||
PLI_RC_PLAYERCOMMENTSSET = 86,
|
||||
PLI_RC_PLAYERBANGET = 87,
|
||||
PLI_RC_PLAYERBANSET = 88,
|
||||
PLI_RC_FILEBROWSER_START = 89,
|
||||
PLI_RC_FILEBROWSER_CD = 90,
|
||||
PLI_RC_FILEBROWSER_END = 91,
|
||||
PLI_RC_FILEBROWSER_DOWN = 92,
|
||||
PLI_RC_FILEBROWSER_UP = 93,
|
||||
PLI_NPCSERVERQUERY = 94,
|
||||
PLI_RC_FILEBROWSER_MOVE = 96,
|
||||
PLI_RC_FILEBROWSER_DELETE = 97,
|
||||
PLI_RC_FILEBROWSER_RENAME = 98,
|
||||
PLI_RC_LARGEFILESTART = 155,
|
||||
PLI_RC_LARGEFILEEND = 156,
|
||||
PLI_RC_FOLDERDELETE = 160,
|
||||
PLI_RC_UNKNOWN162 = 162
|
||||
}
|
||||
|
||||
export enum NCIncomingPacket {
|
||||
PLO_NEWWORLDTIME = 42,
|
||||
PLO_RCCHAT = 74,
|
||||
PLO_NC_CONTROL = 78,
|
||||
PLO_NC_LEVELLIST = 80,
|
||||
PLO_NC_NPCATTRIBUTES = 157,
|
||||
PLO_NC_NPCADD = 158,
|
||||
PLO_NC_NPCDELETE = 159,
|
||||
PLO_NC_NPCSCRIPT = 160,
|
||||
PLO_NC_NPCFLAGS = 161,
|
||||
PLO_NC_CLASSGET = 162,
|
||||
PLO_NC_CLASSADD = 163,
|
||||
PLO_NC_LEVELDUMP = 164,
|
||||
PLO_NC_WEAPONLISTGET = 167,
|
||||
PLO_NC_CLASSDELETE = 188,
|
||||
PLO_NC_WEAPONGET = 192
|
||||
}
|
||||
|
||||
export enum NCOutgoingPacket {
|
||||
PLI_NC_NPCGET = 103,
|
||||
PLI_NC_NPCDELETE = 104,
|
||||
PLI_NC_NPCRESET = 105,
|
||||
PLI_NC_NPCSCRIPTGET = 106,
|
||||
PLI_NC_NPCWARP = 107,
|
||||
PLI_NC_NPCFLAGSGET = 108,
|
||||
PLI_NC_NPCSCRIPTSET = 109,
|
||||
PLI_NC_NPCFLAGSSET = 110,
|
||||
PLI_NC_NPCADD = 111,
|
||||
PLI_NC_CLASSEDIT = 112,
|
||||
PLI_NC_CLASSADD = 113,
|
||||
PLI_NC_LOCALNPCSGET = 114,
|
||||
PLI_NC_WEAPONLISTGET = 115,
|
||||
PLI_NC_WEAPONGET = 116,
|
||||
PLI_NC_WEAPONADD = 117,
|
||||
PLI_NC_WEAPONDELETE = 118,
|
||||
PLI_NC_CLASSDELETE = 119,
|
||||
PLI_NC_LEVELLISTGET = 150,
|
||||
PLI_NC_LEVELLISTSET = 151,
|
||||
}
|
||||
|
||||
export enum PlayerProperties {
|
||||
PLPROP_NICKNAME = 0,
|
||||
PLPROP_ID = 14,
|
||||
PLPROP_ADDITFLAGS = 33,
|
||||
PLPROP_PSTATUSMSG = 53
|
||||
}
|
||||
159
src/serverLister.ts
Normal file
159
src/serverLister.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { GBufferReader, GBufferWriter } from './GBuffer';
|
||||
import { GProtocol, ProtocolGen } from './GProtocol';
|
||||
import { GSocket } from './GSocket';
|
||||
import { PacketTable } from './PacketTable';
|
||||
import { ServerCategory, ServerEntry, ServerlistConfig } from './types';
|
||||
|
||||
function determineServerType(serverName: string): [string, ServerCategory] {
|
||||
switch (serverName.substring(0, 2)) {
|
||||
case 'H ':
|
||||
return [serverName.substring(2).trimLeft(), ServerCategory.hosted];
|
||||
|
||||
case 'P ':
|
||||
return [serverName.substring(2).trimLeft(), ServerCategory.gold];
|
||||
|
||||
case 'U ':
|
||||
return [serverName.substring(2).trimLeft(), ServerCategory.hidden];
|
||||
|
||||
case '3 ':
|
||||
return [serverName.substring(2).trimLeft(), ServerCategory.g3d];
|
||||
}
|
||||
|
||||
return [serverName, ServerCategory.classic];
|
||||
}
|
||||
|
||||
export class Serverlister {
|
||||
private readonly config: ServerlistConfig = {
|
||||
host: 'listserver.graal.in',
|
||||
port: 14922,
|
||||
account: '',
|
||||
password: '',
|
||||
nickname: 'unknown',
|
||||
};
|
||||
|
||||
private packetTable: PacketTable;
|
||||
|
||||
private sock?: GSocket;
|
||||
|
||||
private resolvePromise:
|
||||
| ((value: ServerEntry[] | PromiseLike<ServerEntry[]>) => void)
|
||||
| undefined;
|
||||
// private rejectPromise: ((reason?: any) => void) | undefined;
|
||||
|
||||
constructor(config: Partial<ServerlistConfig>) {
|
||||
this.config = { ...this.config, ...config };
|
||||
this.packetTable = this.initializeHandlers();
|
||||
}
|
||||
|
||||
public static request(
|
||||
config: Partial<ServerlistConfig>,
|
||||
): Promise<ServerEntry[]> {
|
||||
return new Promise<ServerEntry[]>(function (resolve, reject) {
|
||||
const serverList = new Serverlister(config);
|
||||
serverList.resolvePromise = resolve;
|
||||
|
||||
serverList.packetTable.on(4, (id: number, packet: Buffer): void => {
|
||||
const discMsg = packet.toString();
|
||||
reject(discMsg);
|
||||
});
|
||||
|
||||
serverList.sock = GSocket.connect(
|
||||
serverList.config.host,
|
||||
serverList.config.port,
|
||||
{
|
||||
connectCallback: () => serverList.sendLoginPacket(),
|
||||
disconnectCallback: (err: any) => reject(err),
|
||||
packetTable: serverList.packetTable,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private disconnect() {
|
||||
if (this.sock) {
|
||||
this.sock.disconnect();
|
||||
this.sock = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private sendLoginPacket(): void {
|
||||
if (!this.sock) return;
|
||||
|
||||
let nb = GBufferWriter.create();
|
||||
|
||||
// Handshake packet
|
||||
let someKey = GProtocol.generateKey();
|
||||
nb.writeGUInt8(someKey);
|
||||
nb.writeChars('G3D30123');
|
||||
nb.writeChars('rc2');
|
||||
this.sock.sendData(this.sock.sendPacket(7, nb.buffer));
|
||||
this.sock.setProtocol(ProtocolGen.Gen5, someKey);
|
||||
|
||||
// Send credentials
|
||||
nb.clear();
|
||||
nb.writeGString(this.config.account);
|
||||
nb.writeGString(this.config.password);
|
||||
|
||||
this.sock.sendData(this.sock.sendPacket(1, nb.buffer));
|
||||
}
|
||||
|
||||
private initializeHandlers() {
|
||||
const packetTable = new PacketTable();
|
||||
|
||||
// packetTable.setCatchAll((id: number, packet: Buffer): void => {
|
||||
// console.log(`Unhandled Packet (${id}): `, packet);
|
||||
// });
|
||||
|
||||
packetTable.on(0, (id: number, packet: Buffer): void => {
|
||||
let internalBuf = GBufferReader.from(packet);
|
||||
let serverCount = internalBuf.readGUInt8();
|
||||
|
||||
let servers = [];
|
||||
for (let i = 0; i < serverCount; i++) {
|
||||
internalBuf.readGUInt8();
|
||||
|
||||
const serverName = internalBuf.readGString();
|
||||
const serverTypeData = determineServerType(serverName);
|
||||
|
||||
const entry: ServerEntry = {
|
||||
name: serverTypeData[0],
|
||||
category: serverTypeData[1],
|
||||
language: internalBuf.readGString(),
|
||||
description: internalBuf.readGString(),
|
||||
url: internalBuf.readGString(),
|
||||
version: internalBuf.readGString(),
|
||||
pcount: ~~internalBuf.readGString(),
|
||||
ip: internalBuf.readGString(),
|
||||
port: ~~internalBuf.readGString(),
|
||||
};
|
||||
|
||||
servers.push(entry);
|
||||
}
|
||||
|
||||
if (this.resolvePromise) {
|
||||
this.resolvePromise(servers);
|
||||
this.resolvePromise = undefined;
|
||||
|
||||
this.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// packetTable.on(2, (id: number, packet: Buffer): void => {
|
||||
// let statusMsg = packet.toString();
|
||||
// console.log(`Status Msg: ${statusMsg}`);
|
||||
// });
|
||||
|
||||
// packetTable.on(3, (id: number, packet: Buffer): void => {
|
||||
// let siteUrl = packet.toString();
|
||||
// console.log(`Website: ${siteUrl}`);
|
||||
// });
|
||||
|
||||
// packetTable.on(4, (id: number, packet: Buffer): void => {
|
||||
// let discMsg = packet.toString();
|
||||
// console.log(`Disconnected for ${discMsg}`);
|
||||
// this.rejectPromise(discMsg);
|
||||
// });
|
||||
|
||||
return packetTable;
|
||||
}
|
||||
}
|
||||
5
src/tempCodeRunnerFile.ts
Normal file
5
src/tempCodeRunnerFile.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Logger.error(
|
||||
`[RC] Unhandled Packet (${id}): ${packet
|
||||
.toString()
|
||||
.replace(/\r/g, '')}`,
|
||||
);
|
||||
102
src/types.ts
Normal file
102
src/types.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { NPC } from './misc/npcs';
|
||||
|
||||
export enum ServerCategory {
|
||||
classic = 0,
|
||||
gold = 1,
|
||||
hosted = 2,
|
||||
hidden = 3,
|
||||
g3d = 4,
|
||||
}
|
||||
|
||||
export interface ServerEntry {
|
||||
name: string;
|
||||
category: ServerCategory;
|
||||
language: string;
|
||||
description: string;
|
||||
url: string;
|
||||
version: string;
|
||||
pcount: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface ServerlistConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
account: string;
|
||||
password: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
export interface RCEvents {
|
||||
onRCConnected?(instance: RCInterface): void;
|
||||
onRCDisconnected?(instance: RCInterface, text?: string): void;
|
||||
onRCChat?(text: string): void;
|
||||
onFileBrowserMsg?(text: string): void;
|
||||
onRCEventMsg?(text: string): void;
|
||||
}
|
||||
|
||||
export interface RCInterface {
|
||||
get maxUploadFileSize(): number;
|
||||
|
||||
sendRCChat(text: string): void;
|
||||
setNickName(name: string): void;
|
||||
|
||||
requestFolderConfig(): Promise<string>;
|
||||
requestServerFlags(): Promise<string>;
|
||||
requestServerOptions(): Promise<string>;
|
||||
|
||||
// setFolderConfig(text: string): void;
|
||||
// setServerFlags(text: string): void;
|
||||
// setServerOptions(text: string): void;
|
||||
}
|
||||
|
||||
export interface NCEvents {
|
||||
onNCConnected?(): void;
|
||||
onNCDisconnected?(text?: string): void;
|
||||
onNCChat?(text: string): void;
|
||||
|
||||
onNpcAdded?(name: string): void;
|
||||
onNpcDeleted?(name: string): void;
|
||||
}
|
||||
|
||||
export interface NCInterface {
|
||||
get classes(): Set<string>;
|
||||
get npcs(): NPC[];
|
||||
|
||||
requestLevelList(): Promise<string>;
|
||||
|
||||
deleteWeapon(name: string): void;
|
||||
requestWeaponList(): Promise<Set<string>>;
|
||||
requestWeapon(name: string): Promise<[string, string]>;
|
||||
setWeaponScript(name: string, image: string, script: string): void;
|
||||
|
||||
deleteNpc(name: string): void;
|
||||
requestNpcAttributes(name: string): Promise<string>;
|
||||
requestNpcFlags(name: string): Promise<string>;
|
||||
requestNpcScript(name: string): Promise<string>;
|
||||
setNpcFlags(name: string, script: string): void;
|
||||
setNpcScript(name: string, script: string): void;
|
||||
|
||||
deleteClass(name: string): void;
|
||||
requestClass(name: string): Promise<string>;
|
||||
setClassScript(name: string, script: string): void;
|
||||
}
|
||||
|
||||
export enum FSEntryType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
export interface FSEntries {
|
||||
name: string;
|
||||
type: FSEntryType;
|
||||
permissions: string;
|
||||
fileSize: number;
|
||||
modTime: number;
|
||||
}
|
||||
|
||||
export type DirectoryListing = {
|
||||
directory: string;
|
||||
fileList: FSEntries[];
|
||||
};
|
||||
227
src/utils.ts
Normal file
227
src/utils.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
export function hashCode(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = (hash << 5) - hash + str.charCodeAt(i);
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function escapeRegExp(s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
export function graal_tokenize(string: string, sep = ' ') {
|
||||
let separator = sep[0];
|
||||
let insideQuote = false;
|
||||
let stringList = [];
|
||||
let currentString = '';
|
||||
|
||||
let stringLength = string.length;
|
||||
for (let i = 0; i < stringLength; i++) {
|
||||
switch (string[i]) {
|
||||
case separator: {
|
||||
if (!insideQuote) {
|
||||
stringList.push(currentString);
|
||||
currentString = '';
|
||||
} else currentString += string[i];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case '"': {
|
||||
insideQuote = !insideQuote;
|
||||
break;
|
||||
}
|
||||
|
||||
case '\\': {
|
||||
if (i + 1 < stringLength) {
|
||||
switch (string[i + 1]) {
|
||||
case '"':
|
||||
case '\\':
|
||||
i++;
|
||||
|
||||
default:
|
||||
currentString += string[i];
|
||||
break;
|
||||
}
|
||||
} else currentString += string[i];
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
currentString += string[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stringList.push(currentString);
|
||||
return stringList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a string as input, and returns an escaped string replacing new lines
|
||||
* with commas and escaping slashes, and quotes
|
||||
*
|
||||
* @param buffer
|
||||
* @returns
|
||||
*/
|
||||
export function gtokenize(buffer: string): string {
|
||||
let output = '';
|
||||
|
||||
// Remove carriage return characters from the string
|
||||
buffer = buffer.replace(/\r/g, '');
|
||||
|
||||
// Split the buffer by newlines, keeping empty values as well
|
||||
const elements = buffer.split('\n', -1);
|
||||
|
||||
for (let el of elements) {
|
||||
if (el.length > 0) {
|
||||
let complex = false;
|
||||
for (const ch of el) {
|
||||
if (ch < '!' || ch > '~' || ch == ',' || ch == '/') {
|
||||
complex = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (el.trim().length == 0) {
|
||||
complex = true;
|
||||
}
|
||||
|
||||
if (complex) {
|
||||
el = el.replace(/\\/g, '\\\\');
|
||||
el = el.replace(/\"/g, '""');
|
||||
output += '"' + el + '",';
|
||||
} else output += el + ',';
|
||||
} else output += ',';
|
||||
}
|
||||
|
||||
return output.slice(0, -1);
|
||||
}
|
||||
|
||||
export function guntokenize(buffer: string): string {
|
||||
let output = '';
|
||||
let is_paren = false;
|
||||
|
||||
// Check to see if we are starting with a quotation mark.
|
||||
let i = 0;
|
||||
if (buffer[0] == '"') {
|
||||
is_paren = true;
|
||||
++i;
|
||||
}
|
||||
|
||||
// Untokenize.
|
||||
for (; i < buffer.length; ++i) {
|
||||
// If we encounter a comma not inside a quoted string, we are encountering
|
||||
// a new index. Replace the comma with a newline.
|
||||
if (buffer[i] == ',' && !is_paren) {
|
||||
output += '\n';
|
||||
|
||||
// Ignore whitespace.
|
||||
while (i + 1 < buffer.length && buffer[i + 1] == ' ') ++i;
|
||||
|
||||
// Check to see if the next string is quoted.
|
||||
if (i + 1 < buffer.length && buffer[i + 1] == '"') {
|
||||
is_paren = true;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// We need to handle quotation marks as they have different behavior in quoted strings.
|
||||
else if (buffer[i] == '"') {
|
||||
// If we are encountering a quotation mark in a quoted string, we are either
|
||||
// ending the quoted string or escaping a quotation mark.
|
||||
if (is_paren) {
|
||||
if (i + 1 < buffer.length) {
|
||||
// Escaping a quotation mark.
|
||||
if (buffer[i + 1] == '"') {
|
||||
output += '"';
|
||||
++i;
|
||||
}
|
||||
// Ending the quoted string.
|
||||
else if (buffer[i + 1] == ',') is_paren = false;
|
||||
}
|
||||
}
|
||||
// A quotation mark in a non-quoted string.
|
||||
else output += buffer[i];
|
||||
}
|
||||
// Unescape '\' character
|
||||
else if (buffer[i] == '\\') {
|
||||
if (i + 1 < buffer.length && buffer[i + 1] == '\\') {
|
||||
output += '\\';
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Anything else gets put to the output.
|
||||
else output += buffer[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function gCommaStrTokens(buffer: string) {
|
||||
let retData: string[] = [];
|
||||
|
||||
// CString line;
|
||||
let line: string = '';
|
||||
let is_paren = false;
|
||||
|
||||
// // Check to see if we are starting with a quotation mark.
|
||||
let i = 0;
|
||||
if (buffer[0] == '"') {
|
||||
is_paren = true;
|
||||
++i;
|
||||
}
|
||||
|
||||
// // Untokenize.
|
||||
for (; i < buffer.length; ++i) {
|
||||
// If we encounter a comma not inside a quoted string, we are encountering
|
||||
// a new index. Replace the comma with a newline.
|
||||
if (buffer[i] == ',' && !is_paren) {
|
||||
retData.push(line);
|
||||
line = '';
|
||||
|
||||
// Ignore whitespace.
|
||||
while (i + 1 < buffer.length && buffer[i + 1] == ' ') ++i;
|
||||
|
||||
// Check to see if the next string is quoted.
|
||||
if (i + 1 < buffer.length && buffer[i + 1] == '"') {
|
||||
is_paren = true;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
// We need to handle quotation marks as they have different behavior in quoted strings.
|
||||
else if (buffer[i] == '"') {
|
||||
// If we are encountering a quotation mark in a quoted string, we are either
|
||||
// ending the quoted string or escaping a quotation mark.
|
||||
if (is_paren) {
|
||||
if (i + 1 < buffer.length) {
|
||||
// Escaping a quotation mark.
|
||||
if (buffer[i + 1] == '"') {
|
||||
line += '"';
|
||||
++i;
|
||||
}
|
||||
// Ending the quoted string.
|
||||
else if (buffer[i + 1] == ',') is_paren = false;
|
||||
}
|
||||
}
|
||||
// A quotation mark in a non-quoted string.
|
||||
else line += buffer[i];
|
||||
}
|
||||
// Unescape '\' character
|
||||
else if (buffer[i] == '\\') {
|
||||
if (i + 1 < buffer.length) {
|
||||
if (buffer[i + 1] == '\\') {
|
||||
line += '\\';
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Anything else gets put to the output.
|
||||
else line += buffer[i];
|
||||
}
|
||||
|
||||
if (is_paren || line.length > 0) retData.push(line);
|
||||
return retData;
|
||||
}
|
||||
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue