Similarly to Player Info, npc info will require thorough planning. One of the more challenging parts about npc info is providing the properties to the library. Unlike player info, the quantity of NPCs here is significantly higher, and a global positioning system may not be viable.
Problems
How can the library know which NPCs to add to the local scene?
A functional iterator-style interface that yields NPCs within range to be processed by the protocol. The problem here is that the NPCs themselves would be of arbitrary type, so they would either need to implement an interface to yield the necessary properties to the protocol, or they functional interface would need to return small data classes that provide all the necessary information. In either case, this is not ideal.
A repository that keeps track of every NPC's avatar in the game. When a NPC is spawned into the world, the avatar is added to the global collection. When a npc despawns fully (not just in a 'dead' state), their avatar is erased. The downside here is needing to somewhat duplicate the storage of NPCs (once within the server's zones, once within this repository, in a lightweight manner). An additional problem is doing efficient lookups of these avatars. One solution to that is a light-weight zone system that only tracks the NPC avatars and only holds zones which contain a NPC in memory. This would allow us to do zone-based lookups, which is in line with how RS seems to add NPCs.
[!IMPORTANT]
I will likely be going with a Supplier style functional interface. The server will be responsible for finding nearby NPCs and supplying their indices to the library.
There are numerous properties which are transmitted as part of the normal protocol (and not extended info blocks), such as move speed, movement directions, teleportation, direction to face, game cycle on which it spawned and its id. How should the server be providing this information?
NPCs server-side implement a common interface that provides all the properties necessary for the protocol to function. I'd argue this is not ideal, however, as it requires the server to integrate part of the protocol deep within its entities. Additionally, tracking changes to the properties may not be as simple.
A light-weight data class representing each avatar. This would mean the server is responsible of registering and un-registering these avatars when needed.
[!IMPORTANT]
I will likely be using the second option here, where the server registers active NPC avatars. This way the library has all the information it needs to function.
Caching
Unlike players, NPCs do not remain cached post-removal in the client. While the client supports an indices array of up to 65536, the real limiting factor is the number of bits used to inform the client of how many local NPCs to process, which is transmitted using 8 bits. Due to this, the protocol cannot support more than 255 local NPCs. We will utilize extended info caching based on these 255 local NPCs. The cached extended info blocks will always be sent to any players to whom the NPC is shown. If the value is reset, that extended info block is no longer sent for any new observers.
Bitcodes
Due to the high number of NPCs in the world, it is impractical to pre-compute bitcodes for NPCs the same way player info does. On the other hand, however, npc info does however do significantly more individual bit calls.
As a result of it, it may still be beneficial to cache the bit codes, but not pre-compute it for every NPC ahead of time. There are two strategies we could deploy to accomplish this:
On-demand computations. If a npc is being added to the local scene of a player, see if the bitcodes are calculated - if not, calculate and store them. The downside is having to use a lock to do this thread-safely, if an asynchronous implementation is chosen.
Mark zones which occupy players. Pre-process all NPC avatars and see if any players are within a two zone range. Any NPC which intersects with a player will pre-compute the low resolution block, as well as high-resolution if that NPC avatar is tracked by any player. Avatars could keep track of how many observers they currently have. As a result, we would have thread-safe read-only buffer slices for bitcodes. If a NPC is being processed but their bitcodes aren't cached, they would be computed on-demand by all observers. This would primarily occur when a player has increased their view range beyond the usual 15 tile radius.
The expected benefits of bitcode caching would only be seen with a higher number of players. Benchmarking should occur to find the sweet spot, if any, where pre-computations would provide an actual benefit. Prior to reaching that threshold, pre-computations could be disabled, as they would likely result in more processing than just calculating on demand.
Implementation
As explored above, one of the options is for the library to keep track of all NPC avatars. The below implementation assumes this is the path going forward.
Similarly to Player Info, we need to store NPC avatars in a reusable data structure and pool these objects, as they will be quite heavy after extended info blocks are considered. For this, I propose a similar repository to what will be storing Player Info objects.
[!NOTE]
As almost all the code below is the same as with the Player Info implementation, common data structures could be extracted as to avoid reimplementing the same thing with minor changes.
NpcAvatarRepository Snippet
```kt
class NpcAvatarRepository(capacity: Int = DEFAULT_CAPACITY) {
private val avatars: Array = arrayOfNulls(capacity)
private val queue: ReferenceQueue = ReferenceQueue()
fun getOrNull(idx: Int): NpcAvatar? {
return avatars.getOrNull(idx)
}
fun capacity(): Int {
return avatars.size
}
fun alloc(idx: Int): NpcAvatar {
require(idx in avatars.indices) {
"Index out of boundaries: $idx, ${avatars.indices}"
}
val old = avatars[idx]
require(old == null) {
"Overriding existing npc avatar: $idx"
}
val cached = queue.poll()?.get()
if (cached != null) {
cached.reset()
avatars[idx] = cached
return cached
}
val info = NpcAvatar(this, idx.toUShort())
avatars[idx] = info
return info
}
fun dealloc(idx: Int) {
val info = avatars[idx] ?: return
avatars[idx] = null
val reference = SoftReference(info, queue)
reference.enqueue()
}
private companion object {
const val DEFAULT_CAPACITY = 65536
}
}
```
[!NOTE]
The snippet below only demonstrates the properties needed for the protocol to function. Further compression methods can be utilized to significantly compress data structure.
NpcAvatar Snippet
```kt
data class NpcAvatar(
private val repository: NpcAvatarRepository,
private val index: UShort,
) {
private var spawnCycle: Int = 0
private var id: UShort = 0u
private var hidden: Boolean = false
private var teleport: Boolean = false
private var jump: Boolean = false
private var direction: UByte = 0u
private var firstMovementStep: UByte = 0u
private var secondMovementStep: UByte = 0u
private var currentCoordGrid: CoordGrid = CoordGrid.INVALID
private var nextCoordGrid: CoordGrid = CoordGrid.INVALID
private var extendedInfo: NpcAvatarExtendedInfo = NpcAvatarExtendedInfo()
internal fun reset() {
spawnCycle = 0
id = 0u
hidden = false
teleport = false
jump = false
direction = 0u
firstMovementStep = 0u
secondMovementStep = 0u
currentCoordGrid = CoordGrid.INVALID
nextCoordGrid = CoordGrid.INVALID
extendedInfo.reset()
}
}
```
NpcAvatarExtendedInfo Snippet
```kt
class NpcAvatarExtendedInfo {
private var flags: Int = 0
internal fun reset() {
}
fun changeType(id: Int) {
flags = flags or TRANSFORMATION
}
fun faceCoord(
x: Int,
z: Int,
instant: Boolean,
) {
flags = flags or FACE_COORD
}
fun setFacePathingEntity(index: Int) {
flags = flags or FACE_PATHINGENTITY
}
fun setSequence(
id: Int,
delay: Int,
) {
flags = flags or SEQUENCE
}
fun setSpotAnim(
slot: Int,
id: Int,
delay: Int,
height: Int,
) {
flags = flags or SPOTANIM
}
fun setExactMove(
deltaX1: Int,
deltaY1: Int,
delay1: Int,
deltaX2: Int,
deltaY2: Int,
delay2: Int,
direction: Int,
) {
flags = flags or EXACT_MOVE
}
fun addHitMark(
sourceIndex: Int,
selfType: Int,
otherType: Int = selfType,
value: Int,
delay: Int = 0,
) {
flags = flags or HITS
}
fun addSoakedHitMark(
sourceIndex: Int,
selfType: Int,
otherType: Int = selfType,
value: Int,
selfSoakType: Int,
otherSoakType: Int = selfSoakType,
soakValue: Int,
delay: Int = 0,
) {
flags = flags or HITS
}
fun addHeadBar(
id: Int,
fill: Int,
nextFill: Int = fill,
startTime: Int = 0,
endTime: Int = 0,
) {
flags = flags or HITS
}
fun removeHeadBar(id: Int) {
addHeadBar(
id,
fill = 0,
endTime = 0x7FFF,
)
}
fun tinting(
startTime: Int,
endTime: Int,
hue: Int,
saturation: Int,
luminance: Int,
opacity: Int,
local: Boolean = false,
) {
flags = flags or TINTING
}
fun hideOps(flag: Int) {
flags = flags or HIDE_OPS
}
fun hideOps(
op1: Boolean,
op2: Boolean,
op3: Boolean,
op4: Boolean,
op5: Boolean,
) {
val flag =
(if (op1) 0x1 else 0)
.or(if (op2) 0x2 else 0)
.or(if (op3) 0x4 else 0)
.or(if (op4) 0x8 else 0)
.or(if (op5) 0x10 else 0)
hideOps(flag)
}
fun combatLevelChange(newLevel: Int) {
flags = flags or COMBAT_LEVEL_CHANGE
}
fun headiconChange(
slot: Int,
spriteGroup: Int,
spriteIndex: Int,
) {
flags = flags or HEADICON_CHANGE
}
fun nameChange(name: String) {
flags = flags or NAME_CHANGE
}
fun say(text: String) {
flags = flags or SAY
}
fun chatheadCustomisation(
models: List,
modifiedColours: List?,
modifiedTextures: List?,
mirrorPlayer: Boolean,
) {
flags = flags or CHATHEAD_CUSTOMIZATION
}
fun bodyCustomisation(
models: List,
modifiedColours: List?,
modifiedTextures: List?,
mirrorPlayer: Boolean,
) {
flags = flags or BODY_CUSTOMIZATION
}
fun baseAnimationSetChange(
turnLeft: Int = -1,
turnRight: Int = -1,
walk: Int = -1,
walkBack: Int = -1,
walkLeft: Int = -1,
walkRight: Int = -1,
run: Int = -1,
runBack: Int = -1,
runLeft: Int = -1,
runRight: Int = -1,
crawl: Int = -1,
crawlBack: Int = -1,
crawlLeft: Int = -1,
crawlRight: Int = -1,
ready: Int = -1,
) {
flags = flags or NPC_BAS_CHANGE
}
private companion object {
const val TRANSFORMATION = 0x1
const val FACE_COORD = 0x2
const val SEQUENCE = 0x4
const val FACE_PATHINGENTITY = 0x8
const val HITS = 0x10
const val EXTENDED_SHORT = 0x20
const val OLD_SPOTANIM = 0x40
const val SAY = 0x80
const val BODY_CUSTOMIZATION = 0x100
const val CHATHEAD_CUSTOMIZATION = 0x200
const val EXTENDED_MEDIUM = 0x400
const val NAME_CHANGE = 0x800
const val TINTING = 0x1000
const val HIDE_OPS = 0x2000
const val COMBAT_LEVEL_CHANGE = 0x4000
const val EXACT_MOVE = 0x8000
const val HEADICON_CHANGE = 0x10000
const val SPOTANIM = 0x20000
const val NPC_BAS_CHANGE = 0x40000
}
}
```
Npc Info
Similarly to Player Info, npc info will require thorough planning. One of the more challenging parts about npc info is providing the properties to the library. Unlike player info, the quantity of NPCs here is significantly higher, and a global positioning system may not be viable.
Problems
How can the library know which NPCs to add to the local scene?
There are numerous properties which are transmitted as part of the normal protocol (and not extended info blocks), such as move speed, movement directions, teleportation, direction to face, game cycle on which it spawned and its id. How should the server be providing this information?
Caching
Unlike players, NPCs do not remain cached post-removal in the client. While the client supports an indices array of up to 65536, the real limiting factor is the number of bits used to inform the client of how many local NPCs to process, which is transmitted using 8 bits. Due to this, the protocol cannot support more than 255 local NPCs. We will utilize extended info caching based on these 255 local NPCs. The cached extended info blocks will always be sent to any players to whom the NPC is shown. If the value is reset, that extended info block is no longer sent for any new observers.
Bitcodes
Due to the high number of NPCs in the world, it is impractical to pre-compute bitcodes for NPCs the same way player info does. On the other hand, however, npc info does however do significantly more individual bit calls. As a result of it, it may still be beneficial to cache the bit codes, but not pre-compute it for every NPC ahead of time. There are two strategies we could deploy to accomplish this:
The expected benefits of bitcode caching would only be seen with a higher number of players. Benchmarking should occur to find the sweet spot, if any, where pre-computations would provide an actual benefit. Prior to reaching that threshold, pre-computations could be disabled, as they would likely result in more processing than just calculating on demand.
Implementation
As explored above, one of the options is for the library to keep track of all NPC avatars. The below implementation assumes this is the path going forward.
Similarly to Player Info, we need to store NPC avatars in a reusable data structure and pool these objects, as they will be quite heavy after extended info blocks are considered. For this, I propose a similar repository to what will be storing Player Info objects.
NpcAvatarRepository Snippet
```kt class NpcAvatarRepository(capacity: Int = DEFAULT_CAPACITY) { private val avatars: ArrayNpcAvatar Snippet
```kt data class NpcAvatar( private val repository: NpcAvatarRepository, private val index: UShort, ) { private var spawnCycle: Int = 0 private var id: UShort = 0u private var hidden: Boolean = false private var teleport: Boolean = false private var jump: Boolean = false private var direction: UByte = 0u private var firstMovementStep: UByte = 0u private var secondMovementStep: UByte = 0u private var currentCoordGrid: CoordGrid = CoordGrid.INVALID private var nextCoordGrid: CoordGrid = CoordGrid.INVALID private var extendedInfo: NpcAvatarExtendedInfo = NpcAvatarExtendedInfo() internal fun reset() { spawnCycle = 0 id = 0u hidden = false teleport = false jump = false direction = 0u firstMovementStep = 0u secondMovementStep = 0u currentCoordGrid = CoordGrid.INVALID nextCoordGrid = CoordGrid.INVALID extendedInfo.reset() } } ```NpcAvatarExtendedInfo Snippet
```kt class NpcAvatarExtendedInfo { private var flags: Int = 0 internal fun reset() { } fun changeType(id: Int) { flags = flags or TRANSFORMATION } fun faceCoord( x: Int, z: Int, instant: Boolean, ) { flags = flags or FACE_COORD } fun setFacePathingEntity(index: Int) { flags = flags or FACE_PATHINGENTITY } fun setSequence( id: Int, delay: Int, ) { flags = flags or SEQUENCE } fun setSpotAnim( slot: Int, id: Int, delay: Int, height: Int, ) { flags = flags or SPOTANIM } fun setExactMove( deltaX1: Int, deltaY1: Int, delay1: Int, deltaX2: Int, deltaY2: Int, delay2: Int, direction: Int, ) { flags = flags or EXACT_MOVE } fun addHitMark( sourceIndex: Int, selfType: Int, otherType: Int = selfType, value: Int, delay: Int = 0, ) { flags = flags or HITS } fun addSoakedHitMark( sourceIndex: Int, selfType: Int, otherType: Int = selfType, value: Int, selfSoakType: Int, otherSoakType: Int = selfSoakType, soakValue: Int, delay: Int = 0, ) { flags = flags or HITS } fun addHeadBar( id: Int, fill: Int, nextFill: Int = fill, startTime: Int = 0, endTime: Int = 0, ) { flags = flags or HITS } fun removeHeadBar(id: Int) { addHeadBar( id, fill = 0, endTime = 0x7FFF, ) } fun tinting( startTime: Int, endTime: Int, hue: Int, saturation: Int, luminance: Int, opacity: Int, local: Boolean = false, ) { flags = flags or TINTING } fun hideOps(flag: Int) { flags = flags or HIDE_OPS } fun hideOps( op1: Boolean, op2: Boolean, op3: Boolean, op4: Boolean, op5: Boolean, ) { val flag = (if (op1) 0x1 else 0) .or(if (op2) 0x2 else 0) .or(if (op3) 0x4 else 0) .or(if (op4) 0x8 else 0) .or(if (op5) 0x10 else 0) hideOps(flag) } fun combatLevelChange(newLevel: Int) { flags = flags or COMBAT_LEVEL_CHANGE } fun headiconChange( slot: Int, spriteGroup: Int, spriteIndex: Int, ) { flags = flags or HEADICON_CHANGE } fun nameChange(name: String) { flags = flags or NAME_CHANGE } fun say(text: String) { flags = flags or SAY } fun chatheadCustomisation( models: List