Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test
import org.rsmod.api.game.process.player.PlayerZoneUpdateProcessor.Companion.ZONE_VIEW_RADIUS
import org.rsmod.api.registry.loc.LocRegistry
import org.rsmod.api.registry.loc.LocRegistryNormal
import org.rsmod.game.entity.WorldEntityList
import org.rsmod.api.registry.obj.ObjRegistry
import org.rsmod.api.registry.zone.ZoneUpdateMap
import org.rsmod.api.testing.GameTestState
Expand Down Expand Up @@ -47,7 +48,7 @@ class PlayerZoneUpdateProcessorTest {
@Test
fun GameTestState.`process zones in a new build area`() = runAdvancedGameTest {
val parameters = createZoneProcess()
val buildProcessor = PlayerBuildAreaProcessor()
val buildProcessor = PlayerBuildAreaProcessor(WorldEntityList())
val zoneProcessor = parameters.zoneProcessor
val locZones = parameters.locZones
val locRegistry = parameters.normalLocReg
Expand Down Expand Up @@ -148,7 +149,7 @@ class PlayerZoneUpdateProcessorTest {
@Test
fun GameTestState.`only send private obj to receiver`() = runAdvancedGameTest {
val parameters = createZoneProcess()
val buildProcessor = PlayerBuildAreaProcessor()
val buildProcessor = PlayerBuildAreaProcessor(WorldEntityList())
val zoneProcessor = parameters.zoneProcessor
val zoneUpdateMap = parameters.zoneUpdateMap
val objRegistry = parameters.objRegistry
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
package org.rsmod.api.game.process.player

import jakarta.inject.Inject
import org.rsmod.api.utils.map.BuildAreaUtils
import org.rsmod.game.entity.Player
import org.rsmod.game.entity.WorldEntityList
import org.rsmod.map.zone.ZoneKey

public class PlayerBuildAreaProcessor {
public class PlayerBuildAreaProcessor
@Inject
constructor(private val worldEntities: WorldEntityList) {
public fun process(player: Player) {
player.processBuildAreaChange()
}

private fun Player.processBuildAreaChange() {
// Players aboard a world entity keep the root build area anchored to the entity's
// root-world position: their own coords are instance-land coords, but the client's
// root map must follow the boat. No rebuild on embark; recentre only when the
// entity nears the build-area boundary (mid-sail recentre).
val worldEntity = worldEntities.findAt(coords)
if (worldEntity != null) {
val entityCoords = worldEntity.coords
if (BuildAreaUtils.isOutsideOfBuildArea(entityCoords, buildArea)) {
buildArea = BuildAreaUtils.calculateBuildArea(ZoneKey.from(entityCoords))
}
return
}
val rebuildBuildArea = BuildAreaUtils.requiresNewBuildArea(this)
if (rebuildBuildArea) {
enterBuildArea()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.rsmod.api.game.process.player

import jakarta.inject.Inject
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import org.rsmod.api.config.Constants
import org.rsmod.api.npc.isValidTarget
import org.rsmod.api.player.clearInteractionRoute
Expand All @@ -23,6 +26,8 @@ import org.rsmod.api.route.BoundValidator
import org.rsmod.api.route.RayCastValidator
import org.rsmod.events.EventBus
import org.rsmod.game.entity.Player
import org.rsmod.game.entity.WorldEntity
import org.rsmod.game.entity.WorldEntityList
import org.rsmod.game.interact.Interaction
import org.rsmod.game.interact.InteractionLoc
import org.rsmod.game.interact.InteractionLocOp
Expand All @@ -34,13 +39,16 @@ import org.rsmod.game.interact.InteractionObj
import org.rsmod.game.interact.InteractionPlayer
import org.rsmod.game.interact.InteractionPlayerOp
import org.rsmod.game.interact.InteractionPlayerT
import org.rsmod.game.loc.BoundLocInfo
import org.rsmod.game.movement.RouteRequestPathingEntity
import org.rsmod.interact.InteractionStep
import org.rsmod.interact.InteractionTarget
import org.rsmod.interact.Interactions
import org.rsmod.map.CoordGrid
import org.rsmod.routefinder.flag.CollisionFlag

private const val MAX_RIDDEN_OP_RANGE: Int = 8

public class PlayerInteractionProcessor
@Inject
constructor(
Expand All @@ -58,6 +66,7 @@ constructor(
private val playerTInteractions: PlayerTInteractions,
private val protectedAccess: ProtectedAccessLauncher,
private val movement: PlayerMovementProcessor,
private val worldEntities: WorldEntityList,
) {
public fun process(player: Player) {
// Store the current interaction at this stage to ensure that if an interaction triggers a
Expand Down Expand Up @@ -257,9 +266,25 @@ constructor(
validApLine = isWithinApRange(interaction),
)

private fun Player.isWithinOpRange(interaction: InteractionLoc): Boolean =
boundValidator.collides(source = avatar, target = interaction.target) ||
private fun Player.isWithinOpRange(interaction: InteractionLoc): Boolean {
val ridden = worldEntities.findAt(coords)
if (ridden != null && !ridden.contains(interaction.target.coords)) {
return isWithinRiddenOpRange(ridden, interaction.target)
}
return boundValidator.collides(source = avatar, target = interaction.target) ||
boundValidator.touches(source = avatar, target = interaction.target)
}

private fun isWithinRiddenOpRange(ridden: WorldEntity, target: BoundLocInfo): Boolean {
val root = ridden.coords
if (root.level != target.level) {
return false
}
val nearestX = max(target.x, min(root.x, target.x + target.adjustedWidth - 1))
val nearestZ = max(target.z, min(root.z, target.z + target.adjustedLength - 1))
val distance = max(abs(root.x - nearestX), abs(root.z - nearestZ))
return distance <= MAX_RIDDEN_OP_RANGE
}

private fun Player.isWithinApRange(interaction: InteractionLoc): Boolean =
isValidApRange(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.rsmod.api.registry.zone.ZoneUpdateTransformer
import org.rsmod.api.utils.map.BuildAreaUtils
import org.rsmod.api.utils.zone.SharedZoneEnclosedBuffers
import org.rsmod.game.entity.Player
import org.rsmod.game.entity.WorldEntityList
import org.rsmod.game.loc.LocInfo
import org.rsmod.game.obj.Obj
import org.rsmod.map.CoordGrid
Expand All @@ -27,6 +28,7 @@ constructor(
private val locReg: LocRegistry,
private val objReg: ObjRegistry,
private val enclosedBuffers: SharedZoneEnclosedBuffers,
private val worldEntities: WorldEntityList,
) {
public fun computeEnclosedBuffers() {
enclosedBuffers.computeSharedBuffers()
Expand All @@ -45,13 +47,17 @@ constructor(
}

private fun Player.processZoneUpdates() {
val currZone = ZoneKey.from(coords)
// The root-world view center is normally the player's own zone. While aboard a world
// entity, the player's coords are instance-land coords, but their root-world view
// follows the entity's root position - center the visible zones there instead.
val viewCoords = worldEntities.findAt(coords)?.coords ?: coords
val currZone = ZoneKey.from(viewCoords)
val visibleZones = visibleZoneKeys
val prevZone = lastProcessedZone
val prevZone = lastProcessedViewZone
val buildArea = buildArea

if (currZone != prevZone) {
// Compute neighbouring zones based on the player's current zone.
// Compute neighbouring zones based on the player's current view zone.
val currZones =
currZone.computeVisibleNeighbouringZones().filterWithinBuildArea(buildArea)

Expand All @@ -76,7 +82,10 @@ constructor(
processVisibleZoneUpdates(buildArea, visibleZones)
}

lastProcessedZone = currZone
lastProcessedViewZone = currZone
// `lastProcessedZone` must keep tracking the player's REAL zone: zone occupancy
// (PlayerMapUpdateProcessor -> playerRegistry.change) relies on it.
lastProcessedZone = ZoneKey.from(coords)
}

private fun Player.processNewVisibleZones(buildArea: CoordGrid, zones: IntList) {
Expand Down Expand Up @@ -183,29 +192,8 @@ constructor(
client.write(prot)
}

private fun Iterable<ZoneProt>.toPlayerSpecificEnclosed(observerId: Long?): List<ZoneProt> {
val enclosed = ArrayList<ZoneProt>()
for (update in this) {
val prot =
(update as? ZoneUpdateTransformer.PartialFollowsZoneProt)
?.toEnclosed(observerId)
if (prot != null) {
enclosed += prot
}
}
return enclosed
}

private fun ZoneUpdateTransformer.PartialFollowsZoneProt.toEnclosed(
observerId: Long?,
): ZoneProt? =
when (this) {
is ZoneUpdateTransformer.ObjPrivateZoneProt ->
if (isVisibleTo(observerId)) backing else null
is ZoneUpdateTransformer.ObjReveal ->
if (observerId == obj.receiverId) null else backing
else -> backing
}
private fun Iterable<ZoneProt>.toPlayerSpecificEnclosed(observerId: Long?): List<ZoneProt> =
ZoneUpdateTransformer.toObserverEnclosedProtList(this, observerId)

private fun ZoneKey.computeVisibleNeighbouringZones(): IntList {
val zones = IntArrayList(ZONE_VIEW_TOTAL_COUNT)
Expand Down
1 change: 1 addition & 0 deletions api/net/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation(projects.api.script)
implementation(projects.api.serverConfig)
implementation(projects.api.totp)
implementation(projects.api.utils.utilsZone)
implementation(projects.engine.annotations)
implementation(projects.engine.coroutine)
implementation(projects.engine.events)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@ import org.rsmod.api.net.central.OpenRuneCentralWorldLink
import org.rsmod.api.net.central.logging.CentralActivityLogWriter
import org.rsmod.api.net.rsprot.player.SessionStart
import org.rsmod.api.player.events.interact.HeldDropEvents
import org.rsmod.api.registry.loc.LocRegistry
import org.rsmod.api.registry.obj.ObjRegistry
import org.rsmod.api.registry.player.PlayerRegistry
import org.rsmod.api.registry.region.RegionRegistry
import org.rsmod.api.registry.zone.ZoneUpdateMap
import org.rsmod.api.script.onEvent
import org.rsmod.api.server.config.ServerConfig
import org.rsmod.api.utils.zone.SharedZoneEnclosedBuffers
import org.rsmod.game.GameUpdate
import org.rsmod.game.MapClock
import org.rsmod.game.client.Client
import org.rsmod.game.entity.Npc
import org.rsmod.game.entity.Player
import org.rsmod.game.entity.WorldEntity
import org.rsmod.game.entity.WorldEntityList
import org.rsmod.game.entity.npc.NpcStateEvents
import org.rsmod.game.entity.player.SessionStateEvent
import org.rsmod.game.entity.worldentity.WorldEntityStateEvents
import org.rsmod.plugin.scripts.PluginScript
import org.rsmod.plugin.scripts.ScriptContext

Expand All @@ -39,6 +46,11 @@ constructor(
private val database: GameDatabase,
private val characterRepository: CharacterAccountRepository,
private val playerRegistry: PlayerRegistry,
private val worldEntityList: WorldEntityList,
private val zoneUpdates: ZoneUpdateMap,
private val locReg: LocRegistry,
private val objReg: ObjRegistry,
private val enclosedBuffers: SharedZoneEnclosedBuffers,
private val centralActivityLogWriter: CentralActivityLogWriter,
) : PluginScript() {
private val logger = InlineLogger()
Expand Down Expand Up @@ -83,13 +95,18 @@ constructor(
onEvent<SessionStateEvent.Logout> { notifyCentralLogout() }
onEvent<NpcStateEvents.Create> { createNpcAvatar(npc) }
onEvent<NpcStateEvents.Delete> { deleteNpcAvatar(npc) }
onEvent<WorldEntityStateEvents.Create> { createWorldEntityAvatar(entity) }
onEvent<WorldEntityStateEvents.Delete> { deleteWorldEntityAvatar(entity) }
}

private fun initService() {
service.setCommunicationThread(Thread.currentThread())
}

private fun updateService() {
// `infoProtocols.update()` runs worldentity -> player -> npc in order; the npc info
// protocol's first step (`synchronizeModifiedWorlds`) consumes the added/removed
// world entity indices itself - no server-side world sync is needed.
service.infoProtocols.update()
if (openRuneCentral.isEnabled) {
openRuneCentral.drainInboundRevokesOnGameThread(playerRegistry, gameUpdate)
Expand Down Expand Up @@ -137,7 +154,17 @@ constructor(
val infos = service.infoProtocols.alloc(slot, OldSchoolClientType.DESKTOP)

val client = RspClient(session, infos) as Client<Any, Any>
val cycle = RspCycle(session, infos, regionReg)
val cycle =
RspCycle(
session,
infos,
regionReg,
worldEntityList,
zoneUpdates,
locReg,
objReg,
enclosedBuffers,
)

player.client = client
player.clientCycle = cycle
Expand Down Expand Up @@ -215,4 +242,32 @@ constructor(
service.npcAvatarFactory.release(infoProtocol.rspAvatar)
}
}

private fun createWorldEntityAvatar(entity: WorldEntity) {
val rspAvatar =
service.worldEntityAvatarFactory.alloc(
index = entity.slotId,
id = entity.id,
ownerIndex = entity.ownerIndex,
sizeX = entity.sizeX,
sizeZ = entity.sizeZ,
southWestZoneX = entity.southWestZoneX,
southWestZoneZ = entity.southWestZoneZ,
minLevel = entity.minLevel,
maxLevel = entity.maxLevel,
fineX = entity.fineX,
fineZ = entity.fineZ,
projectedLevel = entity.projectedLevel,
activeLevel = entity.activeLevel,
angle = entity.angle,
)
entity.infoProtocol = RspWorldEntityInfo(rspAvatar)
}

private fun deleteWorldEntityAvatar(entity: WorldEntity) {
val infoProtocol = entity.infoProtocol
if (infoProtocol is RspWorldEntityInfo) {
service.worldEntityAvatarFactory.release(infoProtocol.rspAvatar)
}
}
}
Loading
Loading