Skip to content

Commit e6a9691

Browse files
authored
Merge pull request #300 from runejs/fix-map-loading
Working on fixing tilemaps and adding debugging tools for map regions
2 parents d49c43b + 2ef4b0a commit e6a9691

File tree

15 files changed

+343
-94
lines changed

15 files changed

+343
-94
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"dependencies": {
3131
"@hapi/joi": "^16.1.8",
3232
"@runejs/core": "^1.3.2",
33-
"@runejs/filestore": "^0.13.1",
33+
"@runejs/filestore": "^0.13.3",
3434
"@runejs/login-server": "^1.1.0",
3535
"@runejs/update-server": "^1.1.1",
3636
"bcrypt": "^5.0.0",

src/game-engine/net/inbound-packets.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,17 @@ export async function loadPackets(): Promise<Map<number, InboundPacket>> {
3636
return incomingPackets;
3737
}
3838

39-
export function handlePacket(player: Player, packetId: number, packetSize: number, buffer: ByteBuffer): void {
39+
export function handlePacket(player: Player, packetId: number, packetSize: number, buffer: ByteBuffer): boolean {
4040
const incomingPacket = incomingPackets.get(packetId);
4141

4242
if (!incomingPacket) {
4343
logger.info(`Unknown packet ${packetId} with size ${packetSize} received.`);
44-
return;
44+
return false;
4545
}
4646

4747
new Promise<void>(resolve => {
4848
incomingPacket.handler(player, { packetId, packetSize, buffer });
4949
resolve();
5050
}).catch(error => logger.error(`Error handling inbound packet ${packetId} with size ${packetSize}: ${error}`));
51+
return true;
5152
}

src/game-engine/net/server/game-server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ export class GameServerConnection implements SocketConnectionHandler {
8383
this.activeBuffer.copy(packetData, 0, this.activeBuffer.readerIndex, this.activeBuffer.readerIndex + this.activePacketSize);
8484
this.activeBuffer.readerIndex += this.activePacketSize;
8585
}
86-
handlePacket(this.player, this.activePacketId, this.activePacketSize, packetData);
86+
87+
if(!handlePacket(this.player, this.activePacketId, this.activePacketSize, packetData)) {
88+
logger.error(`Player packets out of sync for ${this.player.username}, resetting packet buffer...`);
89+
logger.error(`If you're seeing this, there's a packet that needs fixing. :)`);
90+
clearBuffer = true;
91+
}
8792

8893
if(clearBuffer) {
8994
this.activeBuffer = null;

src/game-engine/world/actor/pathfinding.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,7 @@ export class Pathfinding {
120120
const tiles = [];
121121
for(let x = lowestX; x < highestX; x++) {
122122
for(let y = lowestY; y < highestY; y++) {
123-
let tile = world.chunkManager.tileMap.get(`${x},${y},${this.actor.position.level}`);
124-
if(!tile) {
125-
tile = new Tile(x, y, this.actor.position.level);
126-
tile.bridge = false;
127-
tile.nonWalkable = false;
128-
}
129-
130-
tiles.push(tile);
123+
tiles.push(world.chunkManager.getTile(new Position(x, y, this.actor.position.level)));
131124
}
132125
}
133126

@@ -280,9 +273,9 @@ export class Pathfinding {
280273

281274
public canMoveTo(origin: Position, destination: Position): boolean {
282275
const destinationChunk: Chunk = world.chunkManager.getChunkForWorldPosition(destination);
283-
const tile: Tile = world.chunkManager.tileMap.get(destination.key);
276+
const tile: Tile = world.chunkManager.getTile(destination);
284277

285-
if(tile?.nonWalkable) {
278+
if(tile?.blocked) {
286279
return false;
287280
}
288281

@@ -383,7 +376,7 @@ export class Pathfinding {
383376
public findLocalCornerChunk(cornerX: number, cornerY: number, origin: Position): { localX: number, localY: number, chunk: Chunk } {
384377
const cornerPosition: Position = new Position(cornerX, cornerY, origin.level + 1);
385378
let cornerChunk: Chunk = world.chunkManager.getChunkForWorldPosition(cornerPosition);
386-
const tileAbove: Tile = world.chunkManager.tileMap.get(cornerPosition.key);
379+
const tileAbove: Tile = world.chunkManager.getTile(cornerPosition);
387380
if(!tileAbove?.bridge) {
388381
cornerPosition.level = cornerPosition.level - 1;
389382
cornerChunk = world.chunkManager.getChunkForWorldPosition(cornerPosition);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Options for sending chat messages to a player.
3+
*/
4+
export interface SendMessageOptions {
5+
dialogue?: boolean;
6+
console?: boolean;
7+
}

src/game-engine/world/actor/player/player.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { Quest } from '@engine/world/actor/player/quest';
5454
import { regionChangeActionFactory } from '@engine/world/action/region-change.action';
5555
import { MusicPlayerMode } from '@plugins/music/music-tab.plugin';
5656
import { getVarbitMorphIndex } from '@engine/util/varbits';
57+
import { SendMessageOptions } from '@engine/world/actor/player/model';
5758

5859

5960
export const playerOptions: { option: string, index: number, placement: 'TOP' | 'BOTTOM' }[] = [
@@ -571,17 +572,38 @@ export class Player extends Actor {
571572
}
572573

573574
/**
574-
* Sends a message to the player via the chatbox.
575+
* Sends a message to the player via the chat-box.
575576
* @param messages The single message or array of lines to send to the player.
576577
* @param showDialogue Whether or not to show the message in a "Click to continue" dialogue.
577578
* @returns A Promise<void> that resolves when the player has clicked the "click to continue" button or
578579
* after their chat messages have been sent.
579580
*/
580-
public async sendMessage(messages: string | string[], showDialogue: boolean = false): Promise<boolean> {
581+
public async sendMessage(messages: string | string[], showDialogue?: boolean): Promise<boolean>;
582+
583+
/**
584+
* Sends a message to the player via the chat-box (and the debug console if specified).
585+
* @param messages The single message or array of lines to send to the player.
586+
* @param options A list of options to provide for sending the message - includes values for `dialogue` and `console`
587+
* to enable sending the message as a dialogue message and/or adding the message to the debug console.
588+
* @returns A Promise<void> that resolves when the player has clicked the "click to continue" button or
589+
* after their chat messages have been sent.
590+
*/
591+
public async sendMessage(messages: string | string[], options: SendMessageOptions): Promise<boolean>;
592+
593+
public async sendMessage(messages: string | string[], options: boolean | SendMessageOptions): Promise<boolean> {
581594
if(!Array.isArray(messages)) {
582595
messages = [ messages ];
583596
}
584597

598+
let showDialogue = false;
599+
let showInConsole = false;
600+
if(typeof options === 'boolean') {
601+
showDialogue = true;
602+
} else {
603+
showDialogue = options.dialogue || false;
604+
showInConsole = options.console || false;
605+
}
606+
585607
if(!showDialogue) {
586608
messages.forEach(message => this.outgoingPackets.chatboxMessage(message));
587609
} else {
@@ -593,6 +615,10 @@ export class Player extends Actor {
593615
text => (messages as string[]).join(' ')
594616
]);
595617
}
618+
619+
if(showInConsole) {
620+
messages.forEach(message => this.outgoingPackets.consoleMessage(message));
621+
}
596622
}
597623

598624
/**

src/game-engine/world/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ export class World {
364364
return await this.registerNpc(npc);
365365
}
366366

367+
/**
368+
* Returns the number of remaining open player slots before this world reaches maximum capacity.
369+
*/
370+
public playerSlotsRemaining(): number {
371+
return this.playerList.filter(player => !player).length;
372+
}
373+
367374
public findPlayer(playerUsername: string): Player {
368375
playerUsername = playerUsername.toLowerCase();
369376
return this.playerList?.find(p => p !== null && p.username.toLowerCase() === playerUsername) || null;
@@ -383,6 +390,11 @@ export class World {
383390
}
384391
}
385392

393+
/**
394+
* Registers a new player to the game world.
395+
* Returns false if the world is full, otherwise returns true when the player has been registered.
396+
* @param player The player to register.
397+
*/
386398
public registerPlayer(player: Player): boolean {
387399
if(!player) {
388400
return false;
@@ -400,6 +412,10 @@ export class World {
400412
return true;
401413
}
402414

415+
/**
416+
* Clears the given player's game world slot, signalling that they have disconnected fully.
417+
* @param player The player to remove from the world list.
418+
*/
403419
public deregisterPlayer(player: Player): void {
404420
this.playerList[player.worldIndex] = null;
405421
}

src/game-engine/world/map/chunk-manager.ts

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,27 @@ import { LandscapeFile, LandscapeObject, MapFile } from '@runejs/filestore';
77

88
export class Tile {
99

10-
public settings: number;
11-
public nonWalkable: boolean;
12-
public bridge: boolean;
10+
public settings: number = 0;
11+
public blocked: boolean = false;
12+
public bridge: boolean = false;
1313

1414
public constructor(public x: number, public y: number, public level: number, settings?: number) {
15-
if(settings !== undefined) {
15+
if(settings) {
1616
this.setSettings(settings);
1717
}
1818
}
1919

2020
public setSettings(settings: number): void {
2121
this.settings = settings;
22-
this.nonWalkable = (this.settings & 0x1) == 0x1;
23-
this.bridge = (this.settings & 0x2) == 0x2;
22+
this.blocked = (this.settings & 0x1) === 1;
23+
this.bridge = (this.settings & 0x2) === 2;
2424
}
2525

2626
}
2727

2828
export interface MapRegion {
29-
tiles: Tile[];
3029
objects: LandscapeObject[];
30+
mapFile: MapFile;
3131
}
3232

3333

@@ -37,20 +37,54 @@ export interface MapRegion {
3737
export class ChunkManager {
3838

3939
public readonly regionMap: Map<string, MapRegion> = new Map<string, MapRegion>();
40-
public readonly tileMap: Map<string, Tile> = new Map<string, Tile>();
4140
private readonly chunkMap: Map<string, Chunk>;
4241

4342
public constructor() {
4443
this.chunkMap = new Map<string, Chunk>();
4544
}
4645

46+
public getTile(position: Position): Tile {
47+
const chunkX = position.chunkX + 6;
48+
const chunkY = position.chunkY + 6;
49+
const mapRegionX = Math.floor(chunkX / 8);
50+
const mapRegionY = Math.floor(chunkY / 8);
51+
const mapWorldPositionX = (mapRegionX & 0xff) * 64;
52+
const mapWorldPositionY = mapRegionY * 64;
53+
const regionSettings = this.regionMap.get(`${mapRegionX},${mapRegionY}`)?.mapFile?.tileSettings;
54+
55+
this.registerMapRegion(mapRegionX, mapRegionY);
56+
57+
if(!regionSettings) {
58+
return new Tile(position.x, position.y, position.level);
59+
}
60+
61+
const tileX = position.x - mapWorldPositionX;
62+
const tileY = position.y - mapWorldPositionY;
63+
const tileLevel = position.level;
64+
let tileSettings = regionSettings[tileLevel][tileX][tileY];
65+
66+
if(tileLevel < 3) {
67+
// Check for a bridge tile above the active tile
68+
const tileAboveSettings = regionSettings[tileLevel + 1][tileX][tileY];
69+
if((tileAboveSettings & 0x2) === 2) {
70+
// Set this tile as walkable if the tile above is a bridge -
71+
// This is because the maps are stored with bridges being one level
72+
// above where their collision maps need to be
73+
tileSettings = 0;
74+
}
75+
}
76+
77+
return new Tile(position.x, position.y, tileLevel, tileSettings);
78+
}
79+
4780
public registerMapRegion(mapRegionX: number, mapRegionY: number): void {
4881
const key = `${mapRegionX},${mapRegionY}`;
4982

5083
if(this.regionMap.has(key)) {
5184
// Map region already registered
5285
return;
5386
}
87+
this.regionMap.set(key, null);
5488

5589
let mapFile: MapFile;
5690
let landscapeFile: LandscapeFile;
@@ -66,69 +100,30 @@ export class ChunkManager {
66100
logger.error(`Error decoding landscape file ${mapRegionX},${mapRegionY}`);
67101
}
68102

69-
const region: MapRegion = { tiles: [],
70-
objects: landscapeFile?.landscapeObjects || [] };
71-
72-
// Parse map tiles for game engine use
73-
for(let level = 0; level < 4; level++) {
74-
for(let x = 0; x < 64; x++) {
75-
for(let y = 0; y < 64; y++) {
76-
const tileSettings = mapFile?.tileSettings[level][x][y] || 0;
77-
region.tiles.push(new Tile(x, y, level, tileSettings));
78-
}
79-
}
80-
}
103+
const region: MapRegion = { mapFile, objects: landscapeFile?.landscapeObjects || [] };
81104

82105
this.regionMap.set(key, region);
83-
this.registerTiles(region.tiles);
84-
85-
const worldX = (mapRegionX & 0xff) * 64;
86-
const worldY = mapRegionY * 64;
87-
this.registerObjects(region.objects, worldX, worldY);
106+
this.registerObjects(region.objects, mapFile);
88107
}
89108

90-
public registerTiles(tiles: Tile[]): void {
91-
if(!tiles || tiles.length === 0) {
92-
return;
93-
}
94-
95-
for(const tile of tiles) {
96-
const key = `${tile.x},${tile.y},${tile.level}`;
97-
98-
if(tile.bridge) {
99-
// Move this tile down one level if it's a bridge tile
100-
const newTile = new Tile(tile.x, tile.y, tile.level - 1, 0);
101-
this.tileMap.set(`${newTile.x},${newTile.y},${newTile.level}`, newTile);
102-
103-
// And also add the bridge tile itself, so that game objects know about it
104-
this.tileMap.set(key, tile);
105-
} else if(tile.nonWalkable) { // No need to know about walkable tiles for collision maps, only nonwalkable
106-
if(!this.tileMap.has(key)) {
107-
// Otherwise add a new tile if it hasn't already been set (IE by a bridge tile above)
108-
this.tileMap.set(key, tile);
109-
}
110-
}
111-
}
112-
}
113-
114-
public registerObjects(objects: LandscapeObject[], mapRegionStartX: number, mapRegionStartY: number): void {
109+
public registerObjects(objects: LandscapeObject[], mapFile: MapFile): void {
115110
if(!objects || objects.length === 0) {
116111
return;
117112
}
118113

114+
const mapWorldPositionX = (mapFile.regionX & 0xff) * 64;
115+
const mapWorldPositionY = mapFile.regionY * 64;
116+
119117
for(const object of objects) {
120118
const position = new Position(object.x, object.y, object.level);
119+
const localX = object.x - mapWorldPositionX;
120+
const localY = object.y - mapWorldPositionY;
121121

122-
const tile = this.tileMap.get(position.key);
123-
if(tile?.bridge) {
124-
// Object is on a bridge tile, move it down a level to create proper collision maps
125-
position.level -= 1;
126-
}
127-
128-
const tileAbove = this.tileMap.get(`${object.x},${object.y},${object.level + 1}`);
129-
if(tileAbove?.bridge) {
130-
// Object is below a bridge tile, move it down a level to create proper collision maps
131-
position.level -= 1;
122+
for(let level = 3; level >= 0; level--) {
123+
if((mapFile.tileSettings[level][localX][localY] & 0x2) === 2) {
124+
// Object is on or underneath a bridge tile and needs to move down one level
125+
position.move(object.x, object.y, object.level - 1);
126+
}
132127
}
133128

134129
this.getChunkForWorldPosition(position).setFilestoreLandscapeObject(object);
@@ -175,6 +170,7 @@ export class ChunkManager {
175170
} else {
176171
const chunk = new Chunk(pos);
177172
this.chunkMap.set(pos.key, chunk);
173+
chunk.registerMapRegion();
178174
return chunk;
179175
}
180176
}

src/game-engine/world/map/chunk.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,11 @@ export class Chunk {
3030
this._npcs = [];
3131
this._collisionMap = new CollisionMap(position.x, position.y, position.level, { chunk: this });
3232
this._filestoreLandscapeObjects = new Map<string, LandscapeObject>();
33-
this.registerMapRegion();
3433
}
3534

3635
public registerMapRegion(): void {
37-
const mapRegionX = Math.floor(this.position.x / 8);
38-
const mapRegionY = Math.floor(this.position.y / 8);
39-
36+
const mapRegionX = Math.floor((this.position.x + 6) / 8);
37+
const mapRegionY = Math.floor((this.position.y + 6) / 8);
4038
world.chunkManager.registerMapRegion(mapRegionX, mapRegionY);
4139
}
4240

0 commit comments

Comments
 (0)