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