Skip to content

Commit 8f1e7df

Browse files
authored
Player update and hunting performance (#822)
* Fix invalid area coordinates * Tweaks to improve PlayerUpdateTask performance * Replace CharacterMap with linked list for faster hunting lookup performance (and track player zones) * Fix saving bots on shutdown * Fix duplicate mage of zamorak spawn
1 parent db4f6bb commit 8f1e7df

File tree

17 files changed

+319
-99
lines changed

17 files changed

+319
-99
lines changed

data/area/kandarin/seers_village/seers_village.areas.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ tags = ["penguin_area"]
55
hint = "near where they see."
66

77
[flax_field]
8-
x = [2772, 2728]
8+
x = [2728, 2772]
99
y = [3424, 3455]
1010
tags = ["penguin_area"]
1111
hint = "near the bees and flax."

data/area/misthalin/varrock/varrock.npc-spawns.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ spawns = [
175175
{ id = "giant_rat_varrock", x = 3264, y = 3383, members = true },
176176
{ id = "giant_rat_varrock", x = 3297, y = 3379, members = true },
177177
{ id = "master_farmer_varrock", x = 3231, y = 3347, members = true },
178-
{ id = "mage_of_zamorak_varrock", x = 3260, y = 3383, members = true },
179178
{ id = "tanner_varrock", x = 3188, y = 3407 },
180179
{ id = "dreven", x = 3181, y = 3359 },
181180
{ id = "treznor", x = 3226, y = 3458 },

engine/src/main/kotlin/world/gregs/voidps/engine/client/update/npc/NPCUpdateTask.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import world.gregs.voidps.type.RegionLevel
1616

1717
class NPCUpdateTask(
1818
private val npcs: NPCs,
19-
private val encoders: List<VisualEncoder<NPCVisuals>>,
19+
private val encoders: Array<VisualEncoder<NPCVisuals>>,
2020
) {
2121

2222
fun run(player: Player) {
@@ -112,10 +112,10 @@ class NPCUpdateTask(
112112
var npc: NPC
113113
for (direction in Direction.reversed) {
114114
region = client.tile.regionLevel.add(direction)
115-
for (index in npcs.getDirect(region) ?: continue) {
116-
npc = npcs.indexed(index) ?: continue
115+
npcs.regionMap.onEach(region.id) { index ->
116+
npc = npcs.indexed(index) ?: return@onEach
117117
if (!add(updates, sync, npc, client, viewport, set, index)) {
118-
continue
118+
return@onEach
119119
}
120120
val visuals = npc.visuals
121121
var flag = visuals.flag

engine/src/main/kotlin/world/gregs/voidps/engine/client/update/player/PlayerUpdateTask.kt

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,43 @@ import world.gregs.voidps.engine.client.update.view.Viewport
66
import world.gregs.voidps.engine.entity.character.player.Player
77
import world.gregs.voidps.engine.entity.character.player.Players
88
import world.gregs.voidps.network.login.protocol.encode.updatePlayers
9-
import world.gregs.voidps.network.login.protocol.visual.PlayerVisuals
10-
import world.gregs.voidps.network.login.protocol.visual.VisualEncoder
9+
import world.gregs.voidps.network.login.protocol.visual.VisualMask
1110
import world.gregs.voidps.network.login.protocol.visual.VisualMask.APPEARANCE_MASK
11+
import world.gregs.voidps.network.login.protocol.visual.VisualMask.APPEARANCE_MASK_INV
12+
import world.gregs.voidps.network.login.protocol.visual.encode.SayEncoder
13+
import world.gregs.voidps.network.login.protocol.visual.encode.WatchEncoder
14+
import world.gregs.voidps.network.login.protocol.visual.encode.player.AppearanceEncoder
15+
import world.gregs.voidps.network.login.protocol.visual.encode.player.MovementTypeEncoder
16+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerAnimationEncoder
17+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerColourOverlayEncoder
18+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerExactMovementEncoder
19+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerFaceEncoder
20+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerHitsEncoder
21+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerPrimaryGraphicEncoder
22+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerSecondaryGraphicEncoder
23+
import world.gregs.voidps.network.login.protocol.visual.encode.player.PlayerTimeBarEncoder
24+
import world.gregs.voidps.network.login.protocol.visual.encode.player.TemporaryMoveTypeEncoder
1225
import world.gregs.voidps.type.Delta
1326
import kotlin.math.abs
1427

1528
class PlayerUpdateTask(
1629
private val players: Players,
17-
private val encoders: List<VisualEncoder<PlayerVisuals>>,
1830
) {
19-
20-
private val initialEncoders = encoders.filter { it.initial }
21-
private val initialFlag = initialEncoders.sumOf { it.mask }
31+
private val watchEncoder = WatchEncoder(VisualMask.PLAYER_WATCH_MASK)
32+
private val playerTimeBarEncoder = PlayerTimeBarEncoder()
33+
private val sayEncoder = SayEncoder(VisualMask.PLAYER_SAY_MASK)
34+
private val playerHitsEncoder = PlayerHitsEncoder()
35+
private val playerFaceEncoder = PlayerFaceEncoder()
36+
private val playerExactMovementEncoder = PlayerExactMovementEncoder()
37+
private val playerSecondaryGraphicEncoder = PlayerSecondaryGraphicEncoder()
38+
private val playerColourOverlayEncoder = PlayerColourOverlayEncoder()
39+
private val movementTypeEncoder = MovementTypeEncoder()
40+
private val playerPrimaryGraphicEncoder = PlayerPrimaryGraphicEncoder()
41+
private val playerAnimationEncoder = PlayerAnimationEncoder()
42+
private val appearanceEncoder = AppearanceEncoder()
43+
private val temporaryMoveTypeEncoder = TemporaryMoveTypeEncoder()
44+
45+
private val initialFlag = VisualMask.PLAYER_FACE_MASK + VisualMask.MOVEMENT_TYPE_MASK + VisualMask.PLAYER_ANIMATION_MASK + VisualMask.APPEARANCE_MASK + VisualMask.TEMPORARY_MOVEMENT_TYPE_MASK
2246

2347
fun run(player: Player) {
2448
val viewport = player.viewport ?: return
@@ -85,7 +109,7 @@ class PlayerUpdateTask(
85109
}
86110

87111
encodeMovement(updateType, sync, viewport, player)
88-
encodeVisuals(updates, flag, player, client, set, encoders)
112+
encodeVisuals(updates, flag, player, client, set)
89113
}
90114

91115
if (skip > -1) {
@@ -114,16 +138,49 @@ class PlayerUpdateTask(
114138
viewport.seen(player)
115139
}
116140

117-
private fun encodeVisuals(updates: Writer, flag: Int, player: Player, client: Player, set: PlayerTrackingSet, encoders: List<VisualEncoder<PlayerVisuals>>) {
141+
private fun encodeVisuals(updates: Writer, flag: Int, player: Player, client: Player, set: PlayerTrackingSet) {
118142
if (flag == 0) {
119143
return
120144
}
121145
writeFlag(updates, flag)
122-
for (encoder in encoders) {
123-
if (flag and encoder.mask == 0) {
124-
continue
125-
}
126-
encoder.encode(updates, player.visuals, client.index)
146+
if (flag and VisualMask.PLAYER_WATCH_MASK != 0) {
147+
watchEncoder.encode(updates, player.visuals, client.index)
148+
}
149+
if (flag and VisualMask.PLAYER_TIME_BAR_MASK != 0) {
150+
playerTimeBarEncoder.encode(updates, player.visuals, client.index)
151+
}
152+
if (flag and VisualMask.PLAYER_SAY_MASK != 0) {
153+
sayEncoder.encode(updates, player.visuals, client.index)
154+
}
155+
if (flag and VisualMask.PLAYER_HITS_MASK != 0) {
156+
playerHitsEncoder.encode(updates, player.visuals, client.index)
157+
}
158+
if (flag and VisualMask.PLAYER_FACE_MASK != 0) {
159+
playerFaceEncoder.encode(updates, player.visuals, client.index)
160+
}
161+
if (flag and VisualMask.PLAYER_EXACT_MOVEMENT_MASK != 0) {
162+
playerExactMovementEncoder.encode(updates, player.visuals, client.index)
163+
}
164+
if (flag and VisualMask.PLAYER_GRAPHIC_2_MASK != 0) {
165+
playerSecondaryGraphicEncoder.encode(updates, player.visuals, client.index)
166+
}
167+
if (flag and VisualMask.PLAYER_COLOUR_OVERLAY_MASK != 0) {
168+
playerColourOverlayEncoder.encode(updates, player.visuals, client.index)
169+
}
170+
if (flag and VisualMask.MOVEMENT_TYPE_MASK != 0) {
171+
movementTypeEncoder.encode(updates, player.visuals, client.index)
172+
}
173+
if (flag and VisualMask.PLAYER_GRAPHIC_1_MASK != 0) {
174+
playerPrimaryGraphicEncoder.encode(updates, player.visuals, client.index)
175+
}
176+
if (flag and VisualMask.PLAYER_ANIMATION_MASK != 0) {
177+
playerAnimationEncoder.encode(updates, player.visuals, client.index)
178+
}
179+
if (flag and APPEARANCE_MASK != 0) {
180+
appearanceEncoder.encode(updates, player.visuals, client.index)
181+
}
182+
if (flag and VisualMask.TEMPORARY_MOVEMENT_TYPE_MASK != 0) {
183+
temporaryMoveTypeEncoder.encode(updates, player.visuals, client.index)
127184
}
128185
if (flag and APPEARANCE_MASK != 0) {
129186
set.updateAppearance(player)
@@ -137,7 +194,7 @@ class PlayerUpdateTask(
137194
private fun updateFlag(updates: Writer, player: Player, set: PlayerTrackingSet): Int {
138195
val visuals = player.visuals
139196
if (updates.position() + visuals.appearance.length >= MAX_UPDATE_SIZE) {
140-
return visuals.flag and APPEARANCE_MASK.inv()
197+
return visuals.flag and APPEARANCE_MASK_INV
141198
}
142199
if (set.needsAppearanceUpdate(player)) {
143200
return visuals.flag or APPEARANCE_MASK
@@ -220,7 +277,7 @@ class PlayerUpdateTask(
220277
sync.writeBits(6, player.tile.y and 0x3f)
221278
sync.writeBits(1, appearance)
222279
if (appearance) {
223-
encodeVisuals(updates, initialFlag, player, client, set, initialEncoders)
280+
encodeVisuals(updates, initialFlag, player, client, set)
224281
}
225282
}
226283
if (skip > -1) {
@@ -234,9 +291,9 @@ class PlayerUpdateTask(
234291
* @return true when within [Viewport.radius] and packet has enough room
235292
*/
236293
private fun add(player: Player, client: Player, viewport: Viewport, updates: Writer, sync: Writer): Boolean = player.client?.disconnected != true &&
237-
player.tile.within(client.tile, viewport.radius) &&
238-
updates.position() < MAX_UPDATE_SIZE &&
239-
sync.position() < MAX_SYNC_SIZE
294+
player.tile.within(client.tile, viewport.radius) &&
295+
updates.position() < MAX_UPDATE_SIZE &&
296+
sync.position() < MAX_SYNC_SIZE
240297

241298
fun writeSkip(sync: Writer, skip: Int) {
242299
sync.writeBits(1, 0)

engine/src/main/kotlin/world/gregs/voidps/engine/data/SaveQueue.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import kotlinx.coroutines.*
55
import world.gregs.voidps.engine.client.ui.chat.plural
66
import world.gregs.voidps.engine.entity.character.player.Player
77
import world.gregs.voidps.engine.entity.character.player.Players
8+
import world.gregs.voidps.engine.entity.character.player.name
89
import java.lang.Runnable
910
import java.util.concurrent.ConcurrentHashMap
1011
import kotlin.system.measureTimeMillis
@@ -32,7 +33,7 @@ class SaveQueue(
3233
scope.save(pending.values.toList())
3334
}
3435

35-
fun direct(players: Players): Job = scope.save(players.map { it.copy() })
36+
fun direct(players: Players): Job = scope.save(players.filter { !it.contains("bot") }.map { it.copy() })
3637

3738
private fun CoroutineScope.save(accounts: List<PlayerSave>) = launch(handler) {
3839
val took = measureTimeMillis {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package world.gregs.voidps.engine.entity.character
2+
3+
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap
4+
5+
/**
6+
* High-performance spatial index for grouping character indices by [world.gregs.voidps.type.Zone] or [world.gregs.voidps.type.Region]
7+
* i.e. Map<Zone, List<Index>>
8+
* It's a doubly linked list with head [table] for fast iteration.
9+
*/
10+
class CharacterIndexMap(size: Int) {
11+
private val table = Int2IntOpenHashMap(size)
12+
init {
13+
table.defaultReturnValue(INVALID)
14+
}
15+
val next = IntArray(size) { INVALID }
16+
val previous = IntArray(size) { INVALID }
17+
18+
fun add(id: Int, index: Int) {
19+
previous[index] = INVALID
20+
val head = table.get(id)
21+
next[index] = head
22+
if (head != INVALID) {
23+
previous[head] = index
24+
}
25+
table.put(id, index)
26+
}
27+
28+
fun remove(id: Int, index: Int) {
29+
val p = previous[index]
30+
val n = next[index]
31+
if (p != INVALID) {
32+
next[p] = n
33+
} else {
34+
table.put(id, n)
35+
}
36+
if (n != INVALID) {
37+
previous[n] = p
38+
}
39+
previous[index] = INVALID
40+
next[index] = INVALID
41+
}
42+
43+
fun clear() {
44+
table.clear()
45+
next.fill(INVALID)
46+
previous.fill(INVALID)
47+
}
48+
49+
fun onEach(id: Int, action: (Int) -> Unit) {
50+
var index = table.get(id)
51+
while (index != INVALID) {
52+
action(index)
53+
index = next[index]
54+
}
55+
}
56+
57+
companion object {
58+
private const val INVALID = -1
59+
}
60+
}

engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/CharacterMap.kt

Lines changed: 0 additions & 22 deletions
This file was deleted.

engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/npc/NPCs.kt

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import world.gregs.voidps.engine.data.definition.NPCDefinitions
77
import world.gregs.voidps.engine.entity.Despawn
88
import world.gregs.voidps.engine.entity.MAX_NPCS
99
import world.gregs.voidps.engine.entity.Spawn
10-
import world.gregs.voidps.engine.entity.character.CharacterMap
1110
import world.gregs.voidps.engine.entity.character.CharacterSearch
11+
import world.gregs.voidps.engine.entity.character.CharacterIndexMap
1212
import world.gregs.voidps.engine.entity.character.mode.EmptyMode
1313
import world.gregs.voidps.engine.entity.character.mode.Wander
1414
import world.gregs.voidps.engine.entity.character.player.skill.Skill
@@ -35,7 +35,8 @@ data class NPCs(
3535
private var removeIndex = 0
3636
var size = 0
3737
private set
38-
private val map: CharacterMap = CharacterMap()
38+
private val zoneMap = CharacterIndexMap(MAX_NPCS)
39+
internal val regionMap = CharacterIndexMap(MAX_NPCS)
3940
private val logger = InlineLogger()
4041

4142
override fun run() {
@@ -46,7 +47,8 @@ data class NPCs(
4647
size--
4748
val npc = indexArray[index] ?: continue
4849
indexArray[index] = null
49-
map.remove(npc.tile.regionLevel, npc)
50+
regionMap.remove(npc.tile.regionLevel.id, npc.index)
51+
zoneMap.remove(npc.tile.zone.id, npc.index)
5052
npc.index = -1
5153
}
5254
removeIndex = 0
@@ -90,17 +92,19 @@ data class NPCs(
9092

9193
fun update(npc: NPC, from: Tile) {
9294
if (from.regionLevel != npc.tile.regionLevel) {
93-
map.remove(from.regionLevel, npc)
94-
map.add(npc.tile.regionLevel, npc)
95+
regionMap.remove(from.regionLevel.id, npc.index)
96+
regionMap.add(npc.tile.regionLevel.id, npc.index)
97+
}
98+
if (from.zone != npc.tile.zone) {
99+
zoneMap.remove(from.zone.id, npc.index)
100+
zoneMap.add(npc.tile.zone.id, npc.index)
95101
}
96102
}
97103

98-
fun getDirect(region: RegionLevel): List<Int>? = this.map[region]
99-
100104
override operator fun get(tile: Tile): List<NPC> {
101105
val list = mutableListOf<NPC>()
102-
for (index in map[tile.regionLevel] ?: return list) {
103-
val npc = indexed(index) ?: continue
106+
zoneMap.onEach(tile.zone.id) { index ->
107+
val npc = indexed(index) ?: return@onEach
104108
if (npc.tile == tile) {
105109
list.add(npc)
106110
}
@@ -110,19 +114,16 @@ data class NPCs(
110114

111115
override operator fun get(zone: Zone): List<NPC> {
112116
val list = mutableListOf<NPC>()
113-
for (index in map[zone.regionLevel] ?: return list) {
114-
val npc = indexed(index) ?: continue
115-
if (npc.tile.zone == zone) {
116-
list.add(npc)
117-
}
117+
zoneMap.onEach(zone.id) { index ->
118+
list.add(indexed(index) ?: return@onEach)
118119
}
119120
return list
120121
}
121122

122123
operator fun get(region: RegionLevel): List<NPC> {
123124
val list = mutableListOf<NPC>()
124-
for (index in map[region] ?: return list) {
125-
list.add(indexed(index) ?: continue)
125+
regionMap.onEach(region.id) { index ->
126+
list.add(indexed(index) ?: return@onEach)
126127
}
127128
return list
128129
}
@@ -155,7 +156,8 @@ data class NPCs(
155156
npc.mode = Wander(npc, npc.tile)
156157
}
157158
npc.collision = collision.get(npc)
158-
map.add(npc.tile.regionLevel, npc)
159+
regionMap.add(npc.tile.regionLevel.id, npc.index)
160+
zoneMap.add(npc.tile.zone.id, npc.index)
159161
val respawnDelay = npc.def.getOrNull<Int>("respawn_delay")
160162
if (respawnDelay != null && respawnDelay >= 0) {
161163
npc["respawn_tile"] = npc.tile
@@ -167,7 +169,7 @@ data class NPCs(
167169
}
168170

169171
fun clear(region: RegionLevel) {
170-
for (index in map[region] ?: return) {
172+
regionMap.onEach(region.id) { index ->
171173
if (removeIndex < removeQueue.size) {
172174
removeQueue[removeIndex++] = index
173175
}
@@ -182,6 +184,8 @@ data class NPCs(
182184
indexArray.fill(null)
183185
indexer = 1
184186
size = 0
187+
regionMap.clear()
188+
zoneMap.clear()
185189
}
186190

187191
override fun iterator(): Iterator<NPC> = object : Iterator<NPC> {

0 commit comments

Comments
 (0)