Skip to content

Instantly share code, notes, and snippets.

@ultraviolet-jordan
Created April 7, 2023 16:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ultraviolet-jordan/ec12a9f2f25f4d1d840823e8e8215f5f to your computer and use it in GitHub Desktop.
Save ultraviolet-jordan/ec12a9f2f25f4d1d840823e8e8215f5f to your computer and use it in GitHub Desktop.
@Singleton
class PlayerInfoPacketBuilder @Inject constructor(
private val updateBlocks: PlayerUpdateBlocks
) : PacketBuilder<PlayerInfoPacket>(
opcode = 3,
size = -2
) {
override fun build(packet: PlayerInfoPacket, buffer: RSByteBuffer) {
val players = packet.players
val viewport = packet.viewport
viewport.resize()
buffer.accessBits()
buffer.syncHighDefinition(viewport, players)
buffer.syncLowDefinition(viewport, players)
buffer.accessBytes()
for (index in viewport.highDefinitionUpdates) {
updateBlocks.highDefinitionUpdates[index]?.let { buffer.writeBytes(it) }
}
for (index in viewport.lowDefinitionUpdates) {
updateBlocks.lowDefinitionUpdates[index]?.let { buffer.writeBytes(it) }
}
viewport.reset()
}
private tailrec fun RSByteBuffer.syncHighDefinition(
viewport: Viewport,
players: PlayerList,
nsn: Boolean = true,
index: Int = 0,
skip: Int = -1
) {
if (index == viewport.highDefinitionsCount) {
writeSkipCount(skip)
rewindBits()
if (!nsn) return
return syncHighDefinition(viewport, players, false)
}
val playerIndex = viewport.highDefinitions[index]
if (nsn == (0x1 and viewport.nsnFlags[playerIndex] != 0)) {
return syncHighDefinition(viewport, players, nsn, index + 1, skip)
}
val other = viewport.players[playerIndex]
if (other == null) {
viewport.nsnFlags[playerIndex] = viewport.nsnFlags[playerIndex] or 2
return syncHighDefinition(viewport, players, nsn, index + 1, skip + 1)
}
val removing = viewport.shouldRemove(other, players)
val moving = other.moveDirection != MoveDirection.None
val updating = updateBlocks.highDefinitionUpdates[other.index] != null
val active = removing || updating || moving
if (!active) {
viewport.nsnFlags[playerIndex] = viewport.nsnFlags[playerIndex] or 2
return syncHighDefinition(viewport, players, nsn, index + 1, skip + 1)
}
val offset = writeSkipCount(skip)
writeBits(1, 1)
processHighDefinitionPlayer(viewport, other, playerIndex, removing, moving, updating)
return syncHighDefinition(viewport, players, nsn, index + 1, offset)
}
private tailrec fun RSByteBuffer.syncLowDefinition(
viewport: Viewport,
players: PlayerList,
nsn: Boolean = true,
index: Int = 0,
skip: Int = -1
) {
if (index == viewport.lowDefinitionsCount) {
writeSkipCount(skip)
rewindBits()
if (!nsn) return
return syncLowDefinition(viewport, players, false)
}
val playerIndex = viewport.lowDefinitions[index]
if (nsn == (0x1 and viewport.nsnFlags[playerIndex] == 0)) {
return syncLowDefinition(viewport, players, nsn, index + 1, skip)
}
val other = players[playerIndex]
if (other == null) {
viewport.nsnFlags[playerIndex] = viewport.nsnFlags[playerIndex] or 2
return syncLowDefinition(viewport, players, nsn, index + 1, skip + 1)
}
val adding = viewport.shouldAdd(other)
// We aren't necessarily checking if they are updating (meaning they have update blocks).
// Players always have low definition update blocks, but we are just double-checking
// that the blocks do exist (which they should) so this is for compiler reasons.
// Low definition sync is only checking for if this player needs to be added, and we send
// their update blocks when we add this player from low->high viewport.
val updating = updateBlocks.lowDefinitionUpdates[other.index] != null
val active = adding && updating
if (!active) {
viewport.nsnFlags[playerIndex] = viewport.nsnFlags[playerIndex] or 2
return syncLowDefinition(viewport, players, nsn, index + 1, skip + 1)
}
val offset = writeSkipCount(skip)
writeBits(1, 1)
processLowDefinitionPlayer(viewport, other, playerIndex)
return syncLowDefinition(viewport, players, nsn, index + 1, offset)
}
private fun RSByteBuffer.processHighDefinitionPlayer(
viewport: Viewport,
other: Player,
index: Int,
removing: Boolean,
moving: Boolean,
updating: Boolean
) {
writeBits(1, if (removing) 0 else if (updating) 1 else 0)
when {
removing -> { // remove the player
// send a position update
writeBits(2, 0)
viewport.locations[index] = other.location.regionLocation
validateLocationChanges(viewport, other, index)
viewport.players[index] = null
}
moving -> {
val moveDirection = other.moveDirection
var dx = Direction.DIRECTION_DELTA_X[moveDirection.walkDirection!!.opcode]
var dz = Direction.DIRECTION_DELTA_Z[moveDirection.walkDirection.opcode]
var running = other.moveDirection.runDirection != null
var direction = 0
if (running) {
dx += Direction.DIRECTION_DELTA_X[moveDirection.runDirection!!.opcode]
dz += Direction.DIRECTION_DELTA_Z[moveDirection.runDirection.opcode]
direction = Direction.getPlayerRunningDirection(dx, dz)
running = direction != -1
}
if (!running) {
direction = Direction.getPlayerWalkingDirection(dx, dz)
}
writeBits(2, if (running) 2 else 1) // 2 for running
writeBits(if (running) 4 else 3, direction) // Opcode for direction bit 3 for walking bit 4 for running.
viewport.locations[index] = other.location.regionLocation
}
updating -> {
// send a block update
writeBits(2, 0)
}
}
if (!removing && updating) {
viewport.highDefinitionUpdates += other.index
}
}
private fun RSByteBuffer.processLowDefinitionPlayer(
viewport: Viewport,
other: Player,
index: Int
) {
writeBits(2, 0)
validateLocationChanges(viewport, other, index)
writeBits(13, other.location.x)
writeBits(13, other.location.z)
writeBits(1, 1)
viewport.players[other.index] = other
viewport.nsnFlags[index] = viewport.nsnFlags[index] or 2
viewport.lowDefinitionUpdates += other.index
}
private fun RSByteBuffer.validateLocationChanges(
viewport: Viewport,
other: Player,
index: Int
) {
val currentPacked = viewport.locations[index]
val packed = other.location.regionLocation
val updating = packed != currentPacked
writeBits(1, if (updating) 1 else 0)
if (updating) {
updateCoordinates(currentPacked, packed)
viewport.locations[index] = packed
}
}
private fun RSByteBuffer.updateCoordinates(lastCoordinates: Int, currentCoordinates: Int) {
val lastPlane = lastCoordinates shr 16
val lastRegionX = lastCoordinates shr 8
val lastRegionZ = lastCoordinates and 0xff
val currentPlane = currentCoordinates shr 16
val currentRegionX = currentCoordinates shr 8
val currentRegionZ = currentCoordinates and 0xff
val deltaPlane = currentPlane - lastPlane
val deltaX = currentRegionX - lastRegionX
val deltaZ = currentRegionZ - lastRegionZ
if (lastRegionX == currentRegionX && lastRegionZ == currentRegionZ) {
writeBits(2, 1)
writeBits(2, deltaPlane)
} else if (abs(deltaX) <= 1 && abs(deltaZ) <= 1) {
val opcode = Direction.getDirection(deltaX, deltaZ).opcode
writeBits(2, 2)
writeBits(5, (deltaPlane shl 3) + (opcode and 0x7))
} else {
writeBits(2, 3)
writeBits(18, (deltaZ and 0xff) + (deltaX and 0xff shl 8) + (deltaPlane shl 16))
}
}
private fun RSByteBuffer.writeSkipCount(count: Int): Int {
if (count == -1) return count
writeBits(1, 0)
when {
count == 0 -> writeBits(2, 0)
count < 32 -> {
writeBits(2, 1)
writeBits(5, count)
}
count < 256 -> {
writeBits(2, 2)
writeBits(8, count)
}
count < 2048 -> {
writeBits(2, 3)
writeBits(11, count)
}
}
return -1
}
private fun Viewport.shouldAdd(other: Player?): Boolean = when {
other == null -> false
other == player -> false
other.location == Location.None -> false
!other.location.withinDistance(player.location) -> false
else -> true
}
private fun Viewport.shouldRemove(other: Player?, players: PlayerList): Boolean = when {
other == null -> true
!other.online -> true
other.location == Location.None -> true
!other.location.withinDistance(player.location) -> true
other !in players -> true
else -> false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment