Skip to content

Commit

Permalink
feat: inventory updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Z-Kris committed Apr 8, 2024
1 parent 1fb8185 commit d6613ae
Show file tree
Hide file tree
Showing 12 changed files with 683 additions and 16 deletions.
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ junit = "5.10.2"
jmh = "0.4.10"
inlinelogger = "1.0.6"
log4j = "2.23.1"
commons-pool2 = "2.12.0"

[libraries]
netty-buffer = { module = "io.netty:netty-buffer", version.ref = "netty" }
Expand All @@ -18,6 +19,7 @@ inline-logger = { module = "com.michael-bull.kotlin-inline-logger:kotlin-inline-
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
commons-pool2 = { module = "org.apache.commons:commons-pool2", version.ref = "commons-pool2" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package net.rsprot.protocol.game.outgoing.codec.inv

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.inv.UpdateInvFull
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder
import net.rsprot.protocol.shared.game.outgoing.inv.InventoryObject

public class UpdateInvFullEncoder : MessageEncoder<UpdateInvFull> {
override val prot: ServerProt = GameServerProt.UPDATE_INV_FULL

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateInvFull,
) {
buffer.p4(message.combinedId.combinedId)
buffer.p2(message.inventoryId)
val capacity = message.capacity
buffer.p2(capacity)
for (i in 0..<capacity) {
val obj = message.getObject(i)
if (obj == InventoryObject.NULL) {
buffer.p1Alt1(0)
buffer.p2Alt3(0)
continue
}
val count = obj.count
buffer.p1Alt1(count.coerceAtMost(0xFF))
if (count >= 255) {
buffer.p4Alt2(count)
}
buffer.p2Alt3(obj.id + 1)
}
message.returnInventory()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package net.rsprot.protocol.game.outgoing.codec.inv

import io.netty.channel.ChannelHandlerContext
import net.rsprot.buffer.JagByteBuf
import net.rsprot.protocol.ServerProt
import net.rsprot.protocol.game.outgoing.inv.UpdateInvPartial
import net.rsprot.protocol.game.outgoing.prot.GameServerProt
import net.rsprot.protocol.message.codec.MessageEncoder
import net.rsprot.protocol.metadata.Consistent
import net.rsprot.protocol.shared.game.outgoing.inv.InventoryObject

@Consistent
public class UpdateInvPartialEncoder : MessageEncoder<UpdateInvPartial> {
override val prot: ServerProt = GameServerProt.UPDATE_INV_PARTIAL

override fun encode(
ctx: ChannelHandlerContext,
buffer: JagByteBuf,
message: UpdateInvPartial,
) {
buffer.p4(message.combinedId.combinedId)
buffer.p2(message.inventoryId)
val capacity = message.count
for (i in 0..<capacity) {
val obj = message.getObject(i)
buffer.pSmart1or2(obj.slot)
if (obj == InventoryObject.NULL) {
buffer.p2(0)
continue
}
buffer.p2(obj.id + 1)
val count = obj.count
buffer.p1(count.coerceAtMost(0xFF))
if (count >= 255) {
buffer.p4(count)
}
}
message.returnInventory()
}
}
2 changes: 2 additions & 0 deletions protocol/osrs-221-internal/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ plugins {
dependencies {
implementation(libs.netty.buffer)
implementation(libs.netty.transport)
implementation(libs.commons.pool2)
implementation(libs.inline.logger)
implementation(projects.buffer)
implementation(projects.compression)
implementation(projects.protocol)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package net.rsprot.protocol.internal

import com.github.michaelbull.logging.InlineLogger
import io.netty.util.internal.SystemPropertyUtil

/**
* An internal object that provides easy access to various error-checking flags.
* The purpose of this object is to avoid scattering these checks throughout
* the codebase, making it difficult for users to find any.
* Additionally, requires duplication of code to re-create all this.
*/
public object RSProtFlags {
private val logger: InlineLogger = InlineLogger()
private const val PREFIX = "net.rsprot.protocol.internal."

/**
* Whether the server is in 'development' mode.
* Development mode is effectively a mode where all
* checks are performed to ensure all inputs are validated.
* Users are expected to turn development mode off when
* putting the server into production, as these checks
* end up taking a considerable amount of time.
*/
private val development: Boolean =
getBoolean(
"development",
true,
)

/**
* Whether to check that obj ids in inventory packets are all positive.
*/
public val inventoryObjCheck: Boolean =
getBoolean(
"inventoryObjCheck",
development,
)

/**
* Whether to validate extended info block inputs.
*/
public val extendedInfoInputVerification: Boolean =
getBoolean(
"extendedInfoInputVerification",
development,
)

init {
log("development", development)
log("inventoryObjCheck", inventoryObjCheck)
log("extendedInfoInputVerification", extendedInfoInputVerification)
}

private fun getBoolean(
propertyName: String,
defaultValue: Boolean,
): Boolean {
return SystemPropertyUtil.getBoolean(
PREFIX + propertyName,
defaultValue,
)
}

private fun log(
name: String,
value: Boolean,
) {
logger.debug {
"-D${PREFIX}$name: $value"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package net.rsprot.protocol.internal.game.outgoing.inv.internal

import net.rsprot.protocol.shared.game.outgoing.inv.InventoryObject
import kotlin.jvm.Throws

/**
* A compressed internal representation of an inventory, to be transmitted
* with the various inventory update packets.
* Rather than use a List<Obj>, we pool these [Inventory] instances to avoid
* generating significant amounts of garbage.
* For a popular server, it is perfectly reasonable to expect north of a gigabyte
* of memory to be wasted through List<Obj> instances in the span of an hour.
* We eliminate all garbage generation by using soft-reference pooled inventory
* objects. While this does result in a small hit due to the synchronization involved,
* it is nothing compared to the hit caused by garbage collection and memory allocation
* involved with inventories.
*
* @property count the current count of objs in this inventory
* @property contents the array of contents of this inventory.
* The contents array is initialized at the maximum theoretical size
* of the full inv update packet.
*/
public class Inventory private constructor(
public var count: Int,
private val contents: LongArray,
) {
public constructor(
capacity: Int,
) : this(
0,
LongArray(capacity),
)

/**
* Adds an obj into this inventory
* @param obj the obj to be added to this inventory
* @throws ArrayIndexOutOfBoundsException if the inventory is full
*/
@Throws(ArrayIndexOutOfBoundsException::class)
public fun add(obj: InventoryObject) {
contents[count++] = obj.packed
}

/**
* Gets the obj in [slot].
* @return the obj in the respective slot, or [InventoryObject.NULL]
* if no object exists in that slot.
* @throws ArrayIndexOutOfBoundsException if the index is out of bounds
*/
@Throws(ArrayIndexOutOfBoundsException::class)
public operator fun get(slot: Int): InventoryObject {
return InventoryObject(contents[slot])
}

/**
* Clears the inventory by setting the count to zero.
* The actual backing long array can remain filled with values,
* as those will be overridden by real usages whenever necessary.
*/
public fun clear() {
count = 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package net.rsprot.protocol.internal.game.outgoing.inv.internal

import org.apache.commons.pool2.BasePooledObjectFactory
import org.apache.commons.pool2.ObjectPool
import org.apache.commons.pool2.PooledObject
import org.apache.commons.pool2.PooledObjectFactory
import org.apache.commons.pool2.impl.DefaultPooledObject
import org.apache.commons.pool2.impl.SoftReferenceObjectPool

/**
* A soft-reference based pool of [Inventory] objects, with the primary
* intent being to avoid re-creating lists of objs which end up wasting
* as much as 137kb of memory for a single inventory that's up to 5713 objs
* in capacity. While it is unlikely that any inventory would get near that,
* servers do commonly expand inventory capacities to numbers like 2,000 or 2,500,
* which would still consume up 48-60kb of memory as a result in any traditional manner.
*
* Breakdown of the above statements:
* Assuming an implementation where List<Obj> is provided to the respective packets,
* where Obj is a class of three properties:
*
* ```
* Slot: Int (necessary for partial inv updates)
* Id: Int
* Count: Int
* ```
*
* The resulting memory requirement would be `(12 + (3 * 4))` bytes per obj.
* While this does coincide with the memory alignment,
* it still ends up consuming 24 bytes per obj, all of which would be discarded shortly after.
* Given the assumption that 1,000 players log in at once, and they all have a bank
* of 1000 objs - which is a fairly conservative estimate -, the resulting waste of memory
* is 50 megabytes alone. All of this can be avoided through the use of an object pool,
* as done below.
*/
public data object InventoryPool {
public val pool: ObjectPool<Inventory> = SoftReferenceObjectPool(createFactory())

private fun createFactory(): PooledObjectFactory<Inventory> {
return object : BasePooledObjectFactory<Inventory>() {
override fun create(): Inventory {
// 5713 is the maximum theoretical number of objs an inventory can carry
// before the 40kb limitation could get hit
// This assumes each obj sends a quantity of >= 255
return Inventory(5713)
}

override fun wrap(p0: Inventory): PooledObject<Inventory> {
return DefaultPooledObject(p0)
}
}
}
}
1 change: 1 addition & 0 deletions protocol/osrs-221-model/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
dependencies {
implementation(libs.netty.buffer)
implementation(libs.inline.logger)
implementation(libs.commons.pool2)
implementation(projects.buffer)
implementation(projects.compression)
implementation(projects.crypto)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

package net.rsprot.protocol.game.outgoing.info.playerinfo

import com.github.michaelbull.logging.InlineLogger
import io.netty.buffer.ByteBufAllocator
import io.netty.util.internal.SystemPropertyUtil
import net.rsprot.buffer.JagByteBuf
import net.rsprot.compression.HuffmanCodec
import net.rsprot.protocol.game.outgoing.info.playerinfo.filter.ExtendedInfoFilter
import net.rsprot.protocol.internal.RSProtFlags
import net.rsprot.protocol.internal.game.outgoing.info.playerinfo.extendedinfo.MoveSpeed
import net.rsprot.protocol.internal.game.outgoing.info.playerinfo.extendedinfo.ObjTypeCustomisation
import net.rsprot.protocol.internal.game.outgoing.info.shared.extendedinfo.FacePathingEntity
Expand Down Expand Up @@ -1356,19 +1355,6 @@ public class PlayerAvatarExtendedInfo(
private val UNSIGNED_SHORT_RANGE: IntRange = UShort.MIN_VALUE.toInt()..UShort.MAX_VALUE.toInt()
private val UNSIGNED_SMART_1_OR_2_RANGE: IntRange = 0..0x7FFF

private val logger = InlineLogger()
public val inputVerification: Boolean =
SystemPropertyUtil.getBoolean(
"net.rsprot.protocol.game.outgoing.info.playerinfo.inputVerification",
true,
)

init {
logger.debug {
"-Dnet.rsprot.protocol.game.outgoing.info.playerinfo.inputVerification: $inputVerification"
}
}

/**
* Executes the [block] if input verification is enabled,
* otherwise does nothing. Verification should be enabled for
Expand All @@ -1377,7 +1363,7 @@ public class PlayerAvatarExtendedInfo(
* as there is still some overhead to running verifications.
*/
private inline fun verify(crossinline block: () -> Unit) {
if (inputVerification) {
if (RSProtFlags.extendedInfoInputVerification) {
block()
}
}
Expand Down
Loading

0 comments on commit d6613ae

Please sign in to comment.