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. 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..d939b2195 --- /dev/null +++ b/src/net/data-parser/update-server-parser.ts @@ -0,0 +1,84 @@ +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 + this.clientConnection.socket.write(this.generateFile(index, file)); + break; + case 2: + case 3: // clear queue + this.files = []; + break; + case 4: // error + break; + } + + while(this.files.length > 0) { + const info = this.files.shift(); + this.clientConnection.socket.write(this.generateFile(info.index, info.file)); + } + } + } + + private generateFile(index: number, file: number): Buffer { + 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 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()); - } -}