Due to the complex nature of the player info packet, the library will need to hold most of the state of everyone. It is not feasible to acquire information dynamically as that induces overhead, nor is it viable to build data classes to pass onto the library, as this would be way too costly.
Player Info Repository
Player info repository will provide a way of creating player info instances. These instances will have access to the repository itself as well, in order to look up information of everyone else, as it is a crucial part of player info packet. With this said, player information is actually fairly stand-alone. The only piece of information it will need provided from the server is the coord of everyone, anything else it can handle within itself.
Below is an expected implementation of the repository:
PlayerInfoRepository Snippet
```kt
class PlayerInfoRepository(capacity: Int = DEFAULT_CAPACITY) {
private val infos: Array = arrayOfNulls(capacity)
private val queue: ReferenceQueue = ReferenceQueue()
fun getOrNull(idx: Int): PlayerInfo? {
return infos.getOrNull(idx)
}
fun capacity(): Int {
return infos.size
}
fun alloc(idx: Int): PlayerInfo {
require(idx in infos.indices) {
"Index out of boundaries: $idx, ${infos.indices}"
}
val old = infos[idx]
require(old == null) {
"Overriding existing player info: $idx"
}
val cached = queue.poll()?.get()
if (cached != null) {
cached.reset()
infos[idx] = cached
return cached
}
val info = PlayerInfo(this)
infos[idx] = info
return info
}
fun dealloc(idx: Int) {
val info = infos[idx] ?: return
infos[idx] = null
info.resetReferences()
val reference = SoftReference(info, queue)
reference.enqueue()
}
private companion object {
const val DEFAULT_CAPACITY = 2048
}
}
```
[!TIP]
By default, the player info repository would utilize a reference queue to avoid re-allocating these rather-heavy player info objects. If developers wish, they will be able to opt out of this pooling,
Player Info Implementation
Below is a rough data structure of the player info packet, holding all the state necessary for the protocol to function correctly. Note that this is only a draft to better envision what is needed from the library. The end product may be vastly different.
Avatar Snippet
Avatar class will hold properties about the given local player alone.
```kt
internal data class Avatar(
/**
* The preferred resize range. The player information protocol will attempt to
* add everyone within [preferredResizeRange] tiles to high resolution.
* If [preferredResizeRange] is equal to [Int.MAX_VALUE], resizing will be disabled
* and everyone will be put to high resolution. The extended information may be
* disabled for these players as a result, to avoid buffer overflows.
*/
private var preferredResizeRange: Int = DEFAULT_RESIZE_RANGE,
/**
* The current range at which other players can be observed.
* By default, this value is equal to 15 game squares, however, it may dynamically
* decrease if there are too many high resolution players nearby. It will naturally
* restore back to the default size when the pressure starts to decrease.
*/
private var resizeRange: Int = preferredResizeRange,
/**
* The current cycle counter for resizing logic.
* Resizing by default will occur after every ten cycles. Once the
* protocol begins decrementing the range, it will continue to do so
* every cycle until it reaches a low enough pressure point.
* Every 11th cycle from thereafter, it will attempt to increase it back.
* If it succeeds, it will continue to do so every cycle, similarly to decreasing.
* If it however fails, it will set the range lower by one tile and remain there
* for the next ten cycles.
*/
private var resizeCounter: Int = DEFAULT_RESIZE_INTERVAL,
/**
* The current known coordinate of the given player.
* The coordinate property will need to be updated for all players prior to computing
* player info packet for any of them.
*/
private var currentCoord: CoordGrid = CoordGrid.INVALID,
/**
* The last known coordinate of this player. This property will be used in conjunction
* with [currentCoord] to determine the coordinate delta, which is then transmitted
* to the clients.
*/
private var lastCoord: CoordGrid = CoordGrid.INVALID,
) {
/**
* Resets all the properties to their default state.
* This allows the given [Avatar] to be reused in the future for another player.
*/
fun reset() {
preferredResizeRange = DEFAULT_RESIZE_RANGE
resizeRange = preferredResizeRange
resizeCounter = DEFAULT_RESIZE_INTERVAL
currentCoord = CoordGrid.INVALID
lastCoord = CoordGrid.INVALID
}
internal fun updateCoord(coordGrid: CoordGrid) {
this.currentCoord = coordGrid
}
internal fun postUpdate() {
this.lastCoord = this.currentCoord
}
private companion object {
private const val DEFAULT_RESIZE_RANGE = 15
private const val DEFAULT_RESIZE_INTERVAL = 10
}
}
```
GlobalAvatarGroup Snippet
GlobalAvatarGroup will hold information about every avatar in the game, including the local player.
```kt
internal class GlobalAvatarGroup(capacity: Int) {
/**
* Stationary player flags are used to mark players inactive if they were skipped during processing.
* By utilizing this strategy, the client's code is split into four loops instead of the usual two.
* For both the low resolution players and high resolution players, it will iterate active
* and inactive players separately. This primarily boils down to probabilities:
* If the processed player was inactive in the previous cycle, they are likely inactive
* in this cycle as well. Due to this, the protocol is able to "batch" players together
* more efficiently.
*/
private val stationaryPlayers: ByteArray = ByteArray(capacity)
/**
* Low resolution indices are tracked together with [lowResolutionCount].
* Whenever a player enters the low resolution view, their index
* is added into this [lowResolutionIndices] array, and the [lowResolutionCount]
* is incremented by one.
* At the end of each cycle, the [lowResolutionIndices] are rebuilt to sort the indices.
*/
private val lowResolutionIndices: ShortArray = ShortArray(capacity)
/**
* The number of players in low resolution according to the protocol.
*/
private var lowResolutionCount: Int = 0
/**
* The tracked high resolution players by their indices.
* If a player enters our high resolution, the bit at their index is set to true.
* We do not need to use references to players as we can then refer to the [PlayerInfoRepository]
* to find the actual [PlayerInfo] implementation.
*/
private val highResolutionPlayers: BitSet = BitSet(capacity)
/**
* High resolution indices are tracked together with [highResolutionCount].
* Whenever an external player enters the high resolution view, their index
* is added into this [highResolutionIndices] array, and the [highResolutionCount]
* is incremented by one.
* At the end of each cycle, the [highResolutionIndices] are rebuilt to sort the indices.
*/
private val highResolutionIndices: ShortArray = ShortArray(capacity)
/**
* The number of players in high resolution according to the protocol.
*/
private var highResolutionCount: Int = 0
/**
* Player coordinates are tracked in separate variables. In order to get the full coordinate,
* the two values must be merged. In recent OSRS, the [mapSectors] will only track which quadrant
* of the map the given player is in, where each quadrant is 8192x8192 tiles in size.
* In older versions of OSRS, as well as higher revisions during the pre-eoc era,
* the [mapSectors] would store the mapsquare (64x64 tiles) in which the player is.
* OSRS changed this during Deadman Mode to avoid players observing the number of players
* in nearby mapsquares.
*/
private val mapSectors: IntArray = IntArray(capacity)
/**
* Extended info repository, commonly referred to as "masks", will track everything relevant
* inside itself. Setting properties such as a spotanim would be done through this.
* The [extendedInfoRepository] is also responsible for caching the non-temporary blocks,
* such as appearance and move speed.
*/
private val extendedInfoRepository = ExtendedInfoRepository(capacity)
internal fun reset() {
stationaryPlayers.fill(0)
lowResolutionIndices.fill(0)
lowResolutionCount = 0
highResolutionIndices.fill(0)
highResolutionCount = 0
mapSectors.fill(0)
extendedInfoRepository.reset()
}
internal fun clearTransientExtendedInformation() {
extendedInfoRepository.clearTransientExtendedInformation()
}
private companion object {
private const val CURRENT_CYCLE_INACTIVE = 0x1
private const val NEXT_CYCLE_INACTIVE = 0x2
}
}
```
PlayerInfo Snippet
PlayerInfo will hold references to the local avatar and the global avatar group of this player. This class will directly be responsible for managing extended info updates too, e.g. flagging spotanims will be done through a pre-defined function within this class.
```kt
class PlayerInfo internal constructor(
private val repository: PlayerInfoRepository,
) {
/**
* The [avatar] represents properties of our local player.
*/
private val avatar: Avatar = Avatar()
/**
* The [globalAvatarGroup] represents information about every player in the world.
*/
private val globalAvatarGroup: GlobalAvatarGroup = GlobalAvatarGroup(repository.capacity())
/**
* Reset all the properties of this [PlayerInfo] class.
* This will allow for the [PlayerInfo] to be pooled and re-used later on,
* without needing to deallocate and reallocate it.
*/
internal fun reset() {
avatar.reset()
}
/**
* Synchronize the necessary properties for this player.
* Note: this must be done for all players before calling [prepareBitcodes].
*/
fun synchronize(
x: Int,
z: Int,
level: Int,
) {
this.avatar.updateCoord(CoordGrid(x, z, level))
this.globalAvatarGroup.clearTransientExtendedInformation()
}
/**
* Precalculates all the bitcodes for this player, for both low-resolution and high-resolution updates.
* This function will be thread-safe relative to other players and can be calculated concurrently for all players.
*/
internal fun prepareBitcodes(): Unit = TODO()
/**
* Precalculates all the extended information blocks that are observer-independent.
* Any extended information blocks which rely on the observer will be calculated on-demand during [update].
* This function will be thread-safe relative to other players and can be calculated concurrently for all players.
*/
internal fun prepareExtendedInformation(): Unit = TODO()
/**
* Writes to the actual buffers the prepared bitcodes and extended information.
* This function will be thread-safe relative to other players and can be calculated concurrently for all players.
*/
internal fun update(): Unit = TODO()
/**
* Reset any temporary properties from this cycle.
* Any extended information which doesn't require caching would also be cleared out at this stage.
*/
fun postUpdate() {
this.avatar.postUpdate()
}
}
```
The player info class would be constructed per player in the given world. Extended info buffers will be computed once where applicable (e.g. appearance, which is observer-independent) when an update occurs. Observer-dependent extended info (e.g. hitmarks) will be calculated on-demand by the observer. Some extended info blocks will be subject to temporal caching, such as appearance and move speed. This will be done by tracking the game cycle when the last update occurred. Every observer will additionally keep track of their latest version - if the game cycles align, the extended info block is omitted for that observer, as it is already equivalent to that tracked by the client.
Bitcodes will be generated for all players at the start for both low resolution and high resolution. This is to remain in line with the patent, and to reduce the number of individual bit writes. Instead of writing 2,000 x 2,000 (4 million) bitcodes individually over many bit writes, the 2,000 bitcodes of both variants will be pre-computed at the start, resulting in two short slices for each player. Every observer will then pick the correct bitcode for their given needs and copy the slice of bits over to their buffer in a more efficient manner than one would get by individually writing the respective bits.
Extended Info
A large portion of player info packet is made up by the extended info block, commonly referred to as "masks" in private servers. Extended info will need to meet several criteria:
All extended info blocks must be implemented in their entirety - all functionality provided by the blocks must be modifiable.
Caching needs to be supported by blocks which the client ends up caching. Some examples of this include appearance, move speed and face-locking to other PathingEntities.
Developers must be able to retrieve the latest value at any point, as this is necessary in the case of blocks like Sequence, where the server utilizes a priority system to determine whether the Sequence should be overwritten.
The buffers used by observer-independent extended info blocks must be pre-calculated for every player prior. Any observers will copy the buffer payload over to their respective player info buffer. This will avoid recalculating potentially expensive payloads. Any observer-dependent info blocks will calculate it on demand and will not be subject to caching.
Below is an example of the class with which extended info properties would be assigned:
[!NOTE]
Appearance in the AvatarExtendedInfo snippet below is in an outdated format that is likely scrapped. Discussion on appearance can be found further below.
AvatarExtendedInfo Snippet
AvatarExtendedInfo class will be used as an API to set the state of the given player.
All extended info updates besides appearance are done through functions with arguments for all the properties.
Appearance is done using an internal mutable class as there are way too many properties to pass into a function, and generating new instances of appearance whenever a property changes will result in heavy garbage creation.
When an extended info block is updated, the respective bitflag will automatically be enabled. These bitflags will directly correspond to the ones utilized by the client.
The below snippet has stubs for logic right now, each extended info block itself will have its own dedicated instance with properties corresponding to it (e.g. cacheable, temporary, observer-dependent).
class AvatarExtendedInfo {
@Volatile
@JvmSynthetic
@PublishedApi
internal var flags: Int = 0
fun setMoveSpeed(value: Int) {
flags = flags or MOVE_SPEED
}
fun setTempMoveSpeed(value: Int) {
flags = flags or TEMP_MOVE_SPEED
}
fun setSequence(
id: Int,
delay: Int,
) {
flags = flags or SEQUENCE
}
fun setFacePathingEntity(index: Int) {
flags = flags or FACE_PATHINGENTITY
}
fun setFaceCoord(angle: Int) {
flags = flags or FACE_COORD
}
fun setNameExtras(
beforeName: String?,
afterName: String?,
afterCombatLevel: String?,
) {
flags = flags or NAME_EXTRAS
}
fun setSay(text: String) {
flags = flags or SAY
}
fun setChat(
effects: Int,
modicon: Int,
autotyper: Boolean,
text: String,
pattern: ByteArray?,
) {
flags = flags or CHAT
}
fun setExactMove(
deltaX1: Int,
deltaY1: Int,
delay1: Int,
deltaX2: Int,
deltaY2: Int,
delay2: Int,
direction: Int,
) {
flags = flags or EXACT_MOVE
}
fun setSpotAnim(
slot: Int,
id: Int,
delay: Int,
height: Int,
) {
flags = flags or SPOTANIM
}
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
}
@JvmSynthetic
@PublishedApi
internal inline fun modifyAppearanceInline(block: Appearance.() -> Unit) {
flags = flags or APPEARANCE
}
fun modifyAppearance(consumer: AppearanceConsumer) {
flags = flags or APPEARANCE
}
@PublishedApi
internal companion object {
const val APPEARANCE = 0x1
const val SEQUENCE = 0x2
const val EXTENDED_SHORT = 0x4
const val HITS = 0x8
const val FACE_PATHINGENTITY = 0x20
const val SAY = 0x40
const val FACE_COORD = 0x80
const val TINTING = 0x100
const val MOVE_SPEED = 0x200
const val CHAT = 0x800
const val EXACT_MOVE = 0x1000
const val NAME_EXTRAS = 0x2000
const val EXTENDED_MEDIUM = 0x4000
const val TEMP_MOVE_SPEED = 0x8000
const val SPOTANIM = 0x10000
}
}
Appearance
One of the more challenging parts about extended info is appearance, as it consists of a lot of smaller properties. This leaves us with a dilemma - should the library be responsible for storing all the appearance information in a way that would be accessible to the server as well, or should the server be responsible for maintaining its own source of truth?
As of revision 220, appearance consists of the following properties:
Body type (male, female) (1 byte)
Skull id (1 byte)
Headicon id (1 byte)
Body type
Npc body type (4 bytes)
Player body type (12-24 bytes)
Interface ident kits (12-24 bytes)
Body colours (5 bytes)
Base animation set (14 bytes)
Name (traditional name length is 3-12 bytes, but servers often allow single character names, so 2-13 bytes)
Combat level (2 bytes)
Skill level (2 bytes)
Invisible (1 byte)
Obj type customization (2-74 bytes)
Name extras (3-∞ bytes, as there's no cap other than the 40kb total length requirement)
Gender (1 byte)
Because of the extensive set of properties, likely the best course of action is to allow the server to look up any property on-demand. This would mean servers no longer need to maintain their own their own set of data, especially given how expensive these objects can get (the min sum of everything listed above is already 51 bytes).
Below are some bullet points I wish to achieve with appearance:
Server should be able to look up any property on-demand in an efficient manner.
If any property within appearance is modified, it should automatically notify the extended info blocks that an update is needed. The flagging process should be efficient.
As all caching masks will be storing the built ByteBuf over longer periods of time, the data will effectively be duplicated. Due to the volatile nature of the data, we cannot do lookups out of the buffer without decoding the entire thing, so the data set itself will be stored in memory in two forms - a compressed data class, and a built byte buffer, which is built only once at most, when it is needed - not when the data modifies.
Player Info
Due to the complex nature of the player info packet, the library will need to hold most of the state of everyone. It is not feasible to acquire information dynamically as that induces overhead, nor is it viable to build data classes to pass onto the library, as this would be way too costly.
Player Info Repository
Player info repository will provide a way of creating player info instances. These instances will have access to the repository itself as well, in order to look up information of everyone else, as it is a crucial part of player info packet. With this said, player information is actually fairly stand-alone. The only piece of information it will need provided from the server is the coord of everyone, anything else it can handle within itself.
Below is an expected implementation of the repository:
PlayerInfoRepository Snippet
```kt class PlayerInfoRepository(capacity: Int = DEFAULT_CAPACITY) { private val infos: ArrayPlayer Info Implementation
Below is a rough data structure of the player info packet, holding all the state necessary for the protocol to function correctly. Note that this is only a draft to better envision what is needed from the library. The end product may be vastly different.
Avatar Snippet
Avatar class will hold properties about the given local player alone. ```kt internal data class Avatar( /** * The preferred resize range. The player information protocol will attempt to * add everyone within [preferredResizeRange] tiles to high resolution. * If [preferredResizeRange] is equal to [Int.MAX_VALUE], resizing will be disabled * and everyone will be put to high resolution. The extended information may be * disabled for these players as a result, to avoid buffer overflows. */ private var preferredResizeRange: Int = DEFAULT_RESIZE_RANGE, /** * The current range at which other players can be observed. * By default, this value is equal to 15 game squares, however, it may dynamically * decrease if there are too many high resolution players nearby. It will naturally * restore back to the default size when the pressure starts to decrease. */ private var resizeRange: Int = preferredResizeRange, /** * The current cycle counter for resizing logic. * Resizing by default will occur after every ten cycles. Once the * protocol begins decrementing the range, it will continue to do so * every cycle until it reaches a low enough pressure point. * Every 11th cycle from thereafter, it will attempt to increase it back. * If it succeeds, it will continue to do so every cycle, similarly to decreasing. * If it however fails, it will set the range lower by one tile and remain there * for the next ten cycles. */ private var resizeCounter: Int = DEFAULT_RESIZE_INTERVAL, /** * The current known coordinate of the given player. * The coordinate property will need to be updated for all players prior to computing * player info packet for any of them. */ private var currentCoord: CoordGrid = CoordGrid.INVALID, /** * The last known coordinate of this player. This property will be used in conjunction * with [currentCoord] to determine the coordinate delta, which is then transmitted * to the clients. */ private var lastCoord: CoordGrid = CoordGrid.INVALID, ) { /** * Resets all the properties to their default state. * This allows the given [Avatar] to be reused in the future for another player. */ fun reset() { preferredResizeRange = DEFAULT_RESIZE_RANGE resizeRange = preferredResizeRange resizeCounter = DEFAULT_RESIZE_INTERVAL currentCoord = CoordGrid.INVALID lastCoord = CoordGrid.INVALID } internal fun updateCoord(coordGrid: CoordGrid) { this.currentCoord = coordGrid } internal fun postUpdate() { this.lastCoord = this.currentCoord } private companion object { private const val DEFAULT_RESIZE_RANGE = 15 private const val DEFAULT_RESIZE_INTERVAL = 10 } } ```GlobalAvatarGroup Snippet
GlobalAvatarGroup will hold information about every avatar in the game, including the local player. ```kt internal class GlobalAvatarGroup(capacity: Int) { /** * Stationary player flags are used to mark players inactive if they were skipped during processing. * By utilizing this strategy, the client's code is split into four loops instead of the usual two. * For both the low resolution players and high resolution players, it will iterate active * and inactive players separately. This primarily boils down to probabilities: * If the processed player was inactive in the previous cycle, they are likely inactive * in this cycle as well. Due to this, the protocol is able to "batch" players together * more efficiently. */ private val stationaryPlayers: ByteArray = ByteArray(capacity) /** * Low resolution indices are tracked together with [lowResolutionCount]. * Whenever a player enters the low resolution view, their index * is added into this [lowResolutionIndices] array, and the [lowResolutionCount] * is incremented by one. * At the end of each cycle, the [lowResolutionIndices] are rebuilt to sort the indices. */ private val lowResolutionIndices: ShortArray = ShortArray(capacity) /** * The number of players in low resolution according to the protocol. */ private var lowResolutionCount: Int = 0 /** * The tracked high resolution players by their indices. * If a player enters our high resolution, the bit at their index is set to true. * We do not need to use references to players as we can then refer to the [PlayerInfoRepository] * to find the actual [PlayerInfo] implementation. */ private val highResolutionPlayers: BitSet = BitSet(capacity) /** * High resolution indices are tracked together with [highResolutionCount]. * Whenever an external player enters the high resolution view, their index * is added into this [highResolutionIndices] array, and the [highResolutionCount] * is incremented by one. * At the end of each cycle, the [highResolutionIndices] are rebuilt to sort the indices. */ private val highResolutionIndices: ShortArray = ShortArray(capacity) /** * The number of players in high resolution according to the protocol. */ private var highResolutionCount: Int = 0 /** * Player coordinates are tracked in separate variables. In order to get the full coordinate, * the two values must be merged. In recent OSRS, the [mapSectors] will only track which quadrant * of the map the given player is in, where each quadrant is 8192x8192 tiles in size. * In older versions of OSRS, as well as higher revisions during the pre-eoc era, * the [mapSectors] would store the mapsquare (64x64 tiles) in which the player is. * OSRS changed this during Deadman Mode to avoid players observing the number of players * in nearby mapsquares. */ private val mapSectors: IntArray = IntArray(capacity) /** * Extended info repository, commonly referred to as "masks", will track everything relevant * inside itself. Setting properties such as a spotanim would be done through this. * The [extendedInfoRepository] is also responsible for caching the non-temporary blocks, * such as appearance and move speed. */ private val extendedInfoRepository = ExtendedInfoRepository(capacity) internal fun reset() { stationaryPlayers.fill(0) lowResolutionIndices.fill(0) lowResolutionCount = 0 highResolutionIndices.fill(0) highResolutionCount = 0 mapSectors.fill(0) extendedInfoRepository.reset() } internal fun clearTransientExtendedInformation() { extendedInfoRepository.clearTransientExtendedInformation() } private companion object { private const val CURRENT_CYCLE_INACTIVE = 0x1 private const val NEXT_CYCLE_INACTIVE = 0x2 } } ```PlayerInfo Snippet
PlayerInfo will hold references to the local avatar and the global avatar group of this player. This class will directly be responsible for managing extended info updates too, e.g. flagging spotanims will be done through a pre-defined function within this class. ```kt class PlayerInfo internal constructor( private val repository: PlayerInfoRepository, ) { /** * The [avatar] represents properties of our local player. */ private val avatar: Avatar = Avatar() /** * The [globalAvatarGroup] represents information about every player in the world. */ private val globalAvatarGroup: GlobalAvatarGroup = GlobalAvatarGroup(repository.capacity()) /** * Reset all the properties of this [PlayerInfo] class. * This will allow for the [PlayerInfo] to be pooled and re-used later on, * without needing to deallocate and reallocate it. */ internal fun reset() { avatar.reset() } /** * Synchronize the necessary properties for this player. * Note: this must be done for all players before calling [prepareBitcodes]. */ fun synchronize( x: Int, z: Int, level: Int, ) { this.avatar.updateCoord(CoordGrid(x, z, level)) this.globalAvatarGroup.clearTransientExtendedInformation() } /** * Precalculates all the bitcodes for this player, for both low-resolution and high-resolution updates. * This function will be thread-safe relative to other players and can be calculated concurrently for all players. */ internal fun prepareBitcodes(): Unit = TODO() /** * Precalculates all the extended information blocks that are observer-independent. * Any extended information blocks which rely on the observer will be calculated on-demand during [update]. * This function will be thread-safe relative to other players and can be calculated concurrently for all players. */ internal fun prepareExtendedInformation(): Unit = TODO() /** * Writes to the actual buffers the prepared bitcodes and extended information. * This function will be thread-safe relative to other players and can be calculated concurrently for all players. */ internal fun update(): Unit = TODO() /** * Reset any temporary properties from this cycle. * Any extended information which doesn't require caching would also be cleared out at this stage. */ fun postUpdate() { this.avatar.postUpdate() } } ```The player info class would be constructed per player in the given world. Extended info buffers will be computed once where applicable (e.g. appearance, which is observer-independent) when an update occurs. Observer-dependent extended info (e.g. hitmarks) will be calculated on-demand by the observer. Some extended info blocks will be subject to temporal caching, such as appearance and move speed. This will be done by tracking the game cycle when the last update occurred. Every observer will additionally keep track of their latest version - if the game cycles align, the extended info block is omitted for that observer, as it is already equivalent to that tracked by the client. Bitcodes will be generated for all players at the start for both low resolution and high resolution. This is to remain in line with the patent, and to reduce the number of individual bit writes. Instead of writing 2,000 x 2,000 (4 million) bitcodes individually over many bit writes, the 2,000 bitcodes of both variants will be pre-computed at the start, resulting in two short slices for each player. Every observer will then pick the correct bitcode for their given needs and copy the slice of bits over to their buffer in a more efficient manner than one would get by individually writing the respective bits.
Extended Info
A large portion of player info packet is made up by the extended info block, commonly referred to as "masks" in private servers. Extended info will need to meet several criteria:
Below is an example of the class with which extended info properties would be assigned:
AvatarExtendedInfo class will be used as an API to set the state of the given player. All extended info updates besides appearance are done through functions with arguments for all the properties. Appearance is done using an internal mutable class as there are way too many properties to pass into a function, and generating new instances of appearance whenever a property changes will result in heavy garbage creation.
When an extended info block is updated, the respective bitflag will automatically be enabled. These bitflags will directly correspond to the ones utilized by the client. The below snippet has stubs for logic right now, each extended info block itself will have its own dedicated instance with properties corresponding to it (e.g. cacheable, temporary, observer-dependent).
Appearance
One of the more challenging parts about extended info is appearance, as it consists of a lot of smaller properties. This leaves us with a dilemma - should the library be responsible for storing all the appearance information in a way that would be accessible to the server as well, or should the server be responsible for maintaining its own source of truth?
As of revision 220, appearance consists of the following properties:
Because of the extensive set of properties, likely the best course of action is to allow the server to look up any property on-demand. This would mean servers no longer need to maintain their own their own set of data, especially given how expensive these objects can get (the min sum of everything listed above is already 51 bytes).
Below are some bullet points I wish to achieve with appearance: