From faeea444a74531672c44d0fe83bbb9fb13675f91 Mon Sep 17 00:00:00 2001 From: TheBlackParade Date: Sun, 15 Mar 2020 19:48:41 -0500 Subject: [PATCH 1/3] Implementing the full cache update server. --- package-lock.json | 31 ++++++- package.json | 3 +- src/game-server.ts | 25 +++++- src/net/client-connection.ts | 10 +-- .../data-parser/client-packet-data-parser.ts | 2 - src/net/data-parser/update-server-parser.ts | 86 +++++++++++++++++++ src/net/data-parser/version-list-parser.ts | 35 -------- 7 files changed, 145 insertions(+), 47 deletions(-) create mode 100644 src/net/data-parser/update-server-parser.ts delete mode 100644 src/net/data-parser/version-list-parser.ts diff --git a/package-lock.json b/package-lock.json index e2677fa3b..162c3fbb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,11 +99,12 @@ "dev": true }, "@runejs/cache-parser": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@runejs/cache-parser/-/cache-parser-0.3.0.tgz", - "integrity": "sha512-x/1Auq19zcUd2BGwWHQAxo0PKSFzDYS343UGY01bhUmwnrKKPwNUA6bfp/mx4/FMu3NVs/TARBFXKjG690oUjA==", + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@runejs/cache-parser/-/cache-parser-0.4.5.tgz", + "integrity": "sha512-GkJrd35ITUHtc285wEHf61/IPPrd5dfax3NqFLHmPGBhghL2pMguLwL/sBPHLhVzTDRFkzpu0fHAaynHqZNhRw==", "requires": { "@runejs/logger": "^1.0.0", + "pngjs": "^3.4.0", "seek-bzip": "^1.0.5", "typescript": "^3.7.2" } @@ -998,6 +999,15 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -1201,6 +1211,11 @@ "strip-eof": "^1.0.0" } }, + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -2539,6 +2554,11 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -2551,6 +2571,11 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", diff --git a/package.json b/package.json index 4ca7ed5cb..9f70c38c3 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,11 @@ "license": "GPL-3.0", "dependencies": { "@hapi/joi": "^16.1.8", - "@runejs/cache-parser": "0.3.0", + "@runejs/cache-parser": "0.4.5", "@runejs/logger": "^1.0.0", "bigi": "^1.4.2", "body-parser": "^1.19.0", + "crc-32": "^1.2.0", "express": "^4.17.1", "js-yaml": "^3.13.1", "lodash": "^4.17.15", diff --git a/src/game-server.ts b/src/game-server.ts index 560f900c5..adbb00f95 100644 --- a/src/game-server.ts +++ b/src/game-server.ts @@ -1,5 +1,6 @@ import * as net from 'net'; import { watch } from 'chokidar'; +import * as CRC32 from 'crc-32'; import { RsBuffer } from './net/rs-buffer'; import { World } from './world/world'; @@ -25,10 +26,12 @@ import { setPlayerInitPlugins } from '@server/world/actor/player/player'; import { setNpcInitPlugins } from '@server/world/actor/npc/npc'; import { setQuestPlugins } from '@server/world/config/quests'; + export let serverConfig: ServerConfig; export let gameCache377: EarlyFormatGameCache; export let gameCache: NewFormatGameCache; export let world: World; +export let crcTable: Buffer; export async function injectPlugins(): Promise { const actionTypes: { [key: string]: ActionPlugin[] } = {}; @@ -59,6 +62,20 @@ export async function injectPlugins(): Promise { setNpcInitPlugins(actionTypes[ActionType.NPC_INIT]); } +function generateCrcTable(): void { + const index = gameCache.metaChannel; + const indexLength = index.getBuffer().length; + const buffer = RsBuffer.create(4048); + buffer.writeByte(0); + buffer.writeIntBE(indexLength); + for(let file = 0; file < (indexLength / 6); file++) { + const crcValue = CRC32.buf(gameCache.getRawCacheFile(255, file).getBuffer()); + buffer.writeIntBE(crcValue); + } + + crcTable = buffer.getBuffer(); +} + export function runGameServer(): void { serverConfig = parseServerConfig(); @@ -69,6 +86,7 @@ export function runGameServer(): void { gameCache377 = new EarlyFormatGameCache('cache/377', { loadMaps: true, loadDefinitions: false, loadWidgets: false }); gameCache = new NewFormatGameCache('cache/435'); + generateCrcTable(); world = new World(); injectPlugins().then(() => { world.init(); @@ -87,7 +105,11 @@ export function runGameServer(): void { net.createServer(socket => { logger.info('Socket opened'); - // socket.setNoDelay(true); + + socket.setNoDelay(true); + socket.setKeepAlive(true); + socket.setTimeout(30000); + let clientConnection = new ClientConnection(socket); socket.on('data', data => { @@ -104,6 +126,7 @@ export function runGameServer(): void { }); socket.on('error', error => { + logger.error(error.message); socket.destroy(); logger.error('Socket destroyed due to connection error.'); }); diff --git a/src/net/client-connection.ts b/src/net/client-connection.ts index 025a05a0e..887e4cff0 100644 --- a/src/net/client-connection.ts +++ b/src/net/client-connection.ts @@ -7,11 +7,11 @@ import { ClientLoginParser } from './data-parser/client-login-parser'; import { ClientPacketDataParser } from './data-parser/client-packet-data-parser'; import { DataParser } from './data-parser/data-parser'; import { VersionHandshakeParser } from '@server/net/data-parser/version-handshake-parser'; -import { VersionListParser } from '@server/net/data-parser/version-list-parser'; +import { UpdateServerParser } from '@server/net/data-parser/update-server-parser'; enum ConnectionStage { VERSION_HANDSHAKE = 'VERSION_HANDSHAKE', - VERSION_LIST = 'VERSION_LIST', + UPDATE_SERVER = 'UPDATE_SERVER', LOGIN_HANDSHAKE = 'LOGIN_HANDSHAKE', LOGIN = 'LOGIN', LOGGED_IN = 'LOGGED_IN' @@ -54,8 +54,8 @@ export class ClientConnection { } if(this.connectionStage === ConnectionStage.VERSION_HANDSHAKE) { - this.connectionStage = ConnectionStage.VERSION_LIST; - this.dataParser = new VersionListParser(this); + this.connectionStage = ConnectionStage.UPDATE_SERVER; + this.dataParser = new UpdateServerParser(this); } else if(this.connectionStage === ConnectionStage.LOGIN_HANDSHAKE) { this.connectionStage = ConnectionStage.LOGIN; this.dataParser = new ClientLoginParser(this); @@ -64,9 +64,9 @@ export class ClientConnection { this.dataParser = new ClientPacketDataParser(this); } } catch(err) { - this.socket.destroy(); console.error('Error decoding client data'); console.error(err); + this.socket.destroy(); } } diff --git a/src/net/data-parser/client-packet-data-parser.ts b/src/net/data-parser/client-packet-data-parser.ts index 69bcbe952..137b4f918 100644 --- a/src/net/data-parser/client-packet-data-parser.ts +++ b/src/net/data-parser/client-packet-data-parser.ts @@ -64,8 +64,6 @@ export class ClientPacketDataParser extends DataParser { } if(this.activeBuffer.getReadable() < this.activePacketSize) { - //console.error('Not enough readable data for packet ' + this.activePacketId + ' with size ' + this.activePacketSize + ', but only ' + - // this.activeBuffer.getReadable() + ' data is left of ' + this.activeBuffer.getBuffer().length); return; } diff --git a/src/net/data-parser/update-server-parser.ts b/src/net/data-parser/update-server-parser.ts new file mode 100644 index 000000000..f74319f13 --- /dev/null +++ b/src/net/data-parser/update-server-parser.ts @@ -0,0 +1,86 @@ +import { RsBuffer } from '@server/net/rs-buffer'; +import { DataParser } from './data-parser'; +import { crcTable, gameCache } from '@server/game-server'; + +/** + * Handles the cache update server. + */ +export class UpdateServerParser extends DataParser { + + private files: { file: number, index: number }[] = []; + + public parse(buffer?: RsBuffer): void { + if(!buffer) { + return; + } + + while(buffer.getReadable() >= 4) { + const type = buffer.readUnsignedByte(); + const index = buffer.readUnsignedByte(); + const file = buffer.readUnsignedShortBE(); + + switch(type) { + case 0: // queue + this.files.push({ index, file }); + break; + case 1: // immediate + const data = this.generateFile(index, file).getBuffer(); + this.clientConnection.socket.write(data); + break; + case 2: + case 3: // clear queue + this.files = []; + break; + case 4: // error + break; + } + + while(this.files.length > 0) { + const info = this.files.shift(); + const data = this.generateFile(info.index, info.file); + this.clientConnection.socket.write(data.getBuffer()); + } + } + } + + private generateFile(index: number, file: number): RsBuffer { + let cacheFile; + + if(index === 255 && file === 255) { + const crcBuffer = Buffer.alloc(crcTable.length); + crcTable.copy(crcBuffer, 0, 0); + cacheFile = new RsBuffer(crcBuffer); + } else { + cacheFile = gameCache.getRawCacheFile(index, file); + } + + if(!cacheFile || cacheFile.getBuffer().length === 0) { + throw `Cache file not found; file(${file}) with index(${index})`; + } + + const cacheFileBuffer = cacheFile.getBuffer(); + + const buffer = RsBuffer.create((cacheFileBuffer.length - 2) + ((cacheFileBuffer.length - 2) / 511) + 8); + buffer.writeUnsignedByte(index); + buffer.writeUnsignedShortBE(file); + + let length: number = ((cacheFileBuffer.readUInt8(1) << 24) + (cacheFileBuffer.readUInt8(2) << 16) + + (cacheFileBuffer.readUInt8(3) << 8) + cacheFileBuffer.readUInt8(4)) + 9; + if(cacheFileBuffer[0] == 0) { + length -= 4; + } + + let c = 3; + for(let i = 0; i < length; i++) { + if(c == 512) { + buffer.writeUnsignedByte(255); + c = 1; + } + + buffer.writeByte(cacheFileBuffer.readInt8(i)); + c++; + } + + return new RsBuffer(buffer.getData()); + } +} diff --git a/src/net/data-parser/version-list-parser.ts b/src/net/data-parser/version-list-parser.ts deleted file mode 100644 index d49f9f95c..000000000 --- a/src/net/data-parser/version-list-parser.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { RsBuffer } from '@server/net/rs-buffer'; -import { DataParser } from './data-parser'; - -const VERSION_LIST = [ - 0xff, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x80, 0xfe, 0xbb, 0xa4, 0x5f, - 0x0, 0x0, 0x0, 0x0, 0x2b, 0x3d, 0x5c, 0xd8, 0x0, 0x0, 0x0, 0x0, 0xf9, - 0xb4, 0x1a, 0xe1, 0x0, 0x0, 0x0, 0xfe, 0x5c, 0xb0, 0x6b, 0xd7, 0x0, - 0x0, 0x0, 0x6c, 0x5a, 0x62, 0xe0, 0x19, 0x0, 0x0, 0x0, 0x14, 0xa6, - 0x84, 0x2e, 0x77, 0x0, 0x0, 0x0, 0x54, 0xa, 0xe4, 0x31, 0x30, 0x0, - 0x0, 0x0, 0x0, 0x67, 0xf7, 0x9b, 0x5a, 0x0, 0x0, 0x0, 0x74 -]; - -/** - * Handles the version list transfer with the server. - */ -export class VersionListParser extends DataParser { - - public parse(buffer?: RsBuffer): void { - if(!buffer) { - throw ('No data supplied for version list transfer'); - } - - for(let i = 0; i < 4; i++) { - buffer.readUnsignedByte(); // junk - } - - const outputBuffer = RsBuffer.create(); - - for(const version of VERSION_LIST) { - outputBuffer.writeUnsignedByte(version); - } - - this.clientConnection.socket.write(outputBuffer.getData()); - } -} From 470958074701234ffda9820992a67c57b5f3df1c Mon Sep 17 00:00:00 2001 From: TheBlackParade Date: Sun, 15 Mar 2020 19:51:36 -0500 Subject: [PATCH 2/3] Removing some redundant transformations. --- src/net/data-parser/update-server-parser.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/net/data-parser/update-server-parser.ts b/src/net/data-parser/update-server-parser.ts index f74319f13..d939b2195 100644 --- a/src/net/data-parser/update-server-parser.ts +++ b/src/net/data-parser/update-server-parser.ts @@ -24,8 +24,7 @@ export class UpdateServerParser extends DataParser { this.files.push({ index, file }); break; case 1: // immediate - const data = this.generateFile(index, file).getBuffer(); - this.clientConnection.socket.write(data); + this.clientConnection.socket.write(this.generateFile(index, file)); break; case 2: case 3: // clear queue @@ -37,13 +36,12 @@ export class UpdateServerParser extends DataParser { while(this.files.length > 0) { const info = this.files.shift(); - const data = this.generateFile(info.index, info.file); - this.clientConnection.socket.write(data.getBuffer()); + this.clientConnection.socket.write(this.generateFile(info.index, info.file)); } } } - private generateFile(index: number, file: number): RsBuffer { + private generateFile(index: number, file: number): Buffer { let cacheFile; if(index === 255 && file === 255) { @@ -81,6 +79,6 @@ export class UpdateServerParser extends DataParser { c++; } - return new RsBuffer(buffer.getData()); + return buffer.getData(); } } From dbf174c153dcfd3d8e8795a3260d938127c9c84b Mon Sep 17 00:00:00 2001 From: TheBlackParade Date: Sun, 15 Mar 2020 19:56:16 -0500 Subject: [PATCH 3/3] Updating readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 48b8f55cb..3e8fbe557 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ Currently the server is set up for the 435 revision of the game, which was a gam - NPC spawn loading via YAML configuration. - Player & NPC pathing validation via collision and tile maps generated from the 377 game cache. - A basic REST service for polling logged in users. +- Full functional update server. - A diverse TypeScript plugin system for easily writing new content based off of in-game actions. +- Flexible quest and dialogue systems for easy content development. ## Usage @@ -69,4 +71,4 @@ RuneJS supports the 435 RuneScape game client being renamed by [Promises](https: #### Update Server -RuneJS does not provide a fully working update server for revision 435. While this is planned for a future release, the [refactored-client-435](https://github.com/Promises/refactored-client-435) currently has the update server disabled. Due to this, we can only recommend this client at this time. +RuneJS provides a fully working update server for the 435 client to use. The update server runs alongside the regular game server using the same port, so no additional configuration is required. Simply start the server and then your game client.