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
470 changes: 469 additions & 1 deletion .data/gamevals/dbrow.rscm

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion .data/gamevals/dbtable.rscm
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ music_modern=6532
music_classic=6353
gameframe=6534
mining_rocks=55520
shop_currency=65511
shop_currency=65511
poh_furniture=65506
1 change: 1 addition & 0 deletions .data/gamevals/droptrigger.rscm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
poh_building_mode=0
80 changes: 80 additions & 0 deletions .data/raw-cache/server/inv.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,83 @@ scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_armour_inv"
inherit = "inv.poh_costume_room_armour_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_capes_inv"
inherit = "inv.poh_costume_room_capes_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_magic_wardrobe_inv"
inherit = "inv.poh_costume_room_magic_wardrobe_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_holiday_items_inv"
inherit = "inv.poh_costume_room_holiday_items_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_ame_inv"
inherit = "inv.poh_costume_room_ame_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_treasure_trail_0_inv"
inherit = "inv.poh_costume_room_treasure_trail_0_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_treasure_trail_1_inv"
inherit = "inv.poh_costume_room_treasure_trail_1_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_treasure_trail_2_inv"
inherit = "inv.poh_costume_room_treasure_trail_2_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false

[[inventory]]
isServerOnly = true
id = "inv.poh_costume_room_treasure_trail_3_inv"
inherit = "inv.poh_costume_room_treasure_trail_3_inv"
size = 100
scope = "Perm"
stack = "Always"
protect = false
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ class PlayerZoneUpdateProcessorTest {
}

val startZone = ZoneKey.from(startCoords)
// Zone updates cover every level of the visible zone columns, not just the player's.
val expectedZoneKeys =
zoneRange.flatMap { x -> zoneRange.map { z -> startZone.translate(x, z).packed } }
zoneRange.flatMap { x ->
zoneRange.flatMap { z ->
val zone = startZone.translate(x, z)
(0 until 4).map { level -> ZoneKey(zone.x, zone.z, level).packed }
}
}

// Set a static map loc in loc registry; this loc should not be sent as a zone
// update, or so we will verify.
Expand Down Expand Up @@ -119,7 +125,7 @@ class PlayerZoneUpdateProcessorTest {
process()

// `processNewVisibleZones`
assertEquals(49, captured.countOf<UpdateZoneFullFollows>())
assertEquals(49 * 4, captured.countOf<UpdateZoneFullFollows>())

// `refreshVisibleZoneKeys`
assertEquals(expectedZoneKeys.toSet(), visibleZoneKeys.toSet())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,22 @@ constructor(
else -> backing
}

/**
* Every zone within view range of this zone's column, across **all four levels**. Zone
* updates are level-addressed (`UpdateZoneFullFollows`/`UpdateZonePartialEnclosed` carry a
* level), and the client renders every level of the scene: a loc changed on another level -
* e.g. a house's dungeon staircase replacing its baked hotspot below the player - must reach
* observers on other levels or their client keeps showing the stale baked state until they
* happen to visit that level.
*/
private fun ZoneKey.computeVisibleNeighbouringZones(): IntList {
val zones = IntArrayList(ZONE_VIEW_TOTAL_COUNT)
for (x in -ZONE_VIEW_RADIUS..ZONE_VIEW_RADIUS) {
for (z in -ZONE_VIEW_RADIUS..ZONE_VIEW_RADIUS) {
val zone = translate(x, z)
zones.add(zone.packed)
for (level in 0 until LEVEL_COUNT) {
zones.add(ZoneKey(zone.x, zone.z, level).packed)
}
}
}
return zones
Expand All @@ -234,8 +244,9 @@ constructor(

public companion object {
public const val ZONE_VIEW_RADIUS: Int = 3
public const val LEVEL_COUNT: Int = 4
public const val ZONE_VIEW_TOTAL_COUNT: Int =
(2 * ZONE_VIEW_RADIUS + 1) * (2 * ZONE_VIEW_RADIUS + 1)
(2 * ZONE_VIEW_RADIUS + 1) * (2 * ZONE_VIEW_RADIUS + 1) * LEVEL_COUNT

public val BUILD_AREA_BOUNDS: IntRange = 0 until BuildAreaUtils.SIZE
}
Expand Down
19 changes: 19 additions & 0 deletions api/poh/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id("base-conventions")
}

dependencies {
implementation(libs.guice)
implementation("com.google.code.gson:gson:2.13.2")
implementation(projects.api.attr)
implementation(projects.api.player)
implementation(projects.api.registry)
implementation(projects.api.repo)
implementation(projects.api.script)
implementation(projects.engine.events)
implementation(projects.engine.game)
implementation(projects.engine.map)
implementation(projects.engine.module)
implementation(projects.engine.plugin)
implementation(projects.engine.routefinder)
}
94 changes: 94 additions & 0 deletions api/poh/src/main/kotlin/org/rsmod/api/poh/PohAttributes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.rsmod.api.poh

import dev.openrune.rscm.RSCM
import dev.openrune.rscm.RSCM.asRSCM
import dev.openrune.rscm.RSCMType
import org.rsmod.api.attr.AttributeKey
import org.rsmod.api.player.stat.baseConstructionLvl
import org.rsmod.api.player.vars.intVarBit
import org.rsmod.game.entity.Player

/**
* House layout persistence through the `character_attrs` JSON path.
* - [ROOMS]: `"<level>_<gridX>_<gridZ>" -> roomTypeIndex or (rotation shl 16)`
* - [FURNITURE]: `"<level>_<gridX>_<gridZ>_<hotspotIndex>" -> furniture loc id`
*
* Room type and hotspot indices are the stable indices of the datagen resources, guarded by
* `poh_index_manifest.json`. House size / style / location / servant state live in the real varbits
* and persist through varp persistence - they are never duplicated here.
*/
public object PohAttributes {
public val ROOMS: AttributeKey<MutableMap<String, Int>> =
AttributeKey(persistenceKey = "poh_rooms")

public val FURNITURE: AttributeKey<MutableMap<String, Int>> =
AttributeKey(persistenceKey = "poh_furniture")

/**
* Packed [org.rsmod.map.CoordGrid] to relocate to on the next login (set on in-house logout).
*/
public val LOGIN_EXIT_COORD: AttributeKey<Int> =
AttributeKey(persistenceKey = "poh_login_exit_coord")

private const val ROTATION_SHIFT: Int = 16
private const val ROOM_INDEX_MASK: Int = (1 shl ROTATION_SHIFT) - 1

public fun encodeRoom(roomTypeIndex: Int, rotation: Int): Int =
roomTypeIndex or (rotation shl ROTATION_SHIFT)

public fun decodeRoomIndex(value: Int): Int = value and ROOM_INDEX_MASK

public fun decodeRoomRotation(value: Int): Int = (value shr ROTATION_SHIFT) and 0x3

/** Decodes the persisted house layout, or `null` when the player owns no house. */
public fun load(player: Player, dataStore: PohDataStore): PohHouse? {
val location = player.pohHouseLocation
if (location == 0) {
return null
}
val house =
PohHouse(
size = PohConstants.gridSize(player.constructionLevelForHouse()),
style = player.pohHouseStyle,
location = location,
)
val rooms = player.attr[ROOMS].orEmpty()
for ((key, value) in rooms) {
val slot = PohRoomSlot.fromPersistenceKey(key) ?: continue
val room = dataStore.room(decodeRoomIndex(value))
house.rooms[slot] = PohRoom(room.key, decodeRoomRotation(value))
}
val furniture = player.attr[FURNITURE].orEmpty()
for ((key, value) in furniture) {
val slot = PohHotspotSlot.fromPersistenceKey(key) ?: continue
house.furniture[slot] = RSCM.getReverseMapping(RSCMType.LOC, value)
}
return house
}

/** Writes the full house layout back into the persistent attribute blobs. */
public fun save(player: Player, house: PohHouse, dataStore: PohDataStore) {
val rooms = mutableMapOf<String, Int>()
for ((slot, room) in house.rooms) {
val type = dataStore.room(room.type)
rooms[slot.persistenceKey()] = encodeRoom(type.index, room.rotation)
}
player.attr[ROOMS] = rooms

val furniture = mutableMapOf<String, Int>()
for ((slot, loc) in house.furniture) {
furniture[slot.persistenceKey()] = loc.asRSCM(RSCMType.LOC)
}
player.attr[FURNITURE] = furniture
}
}

public var Player.pohHouseLocation: Int by intVarBit("varbit.poh_house_location")
public var Player.pohHouseStyle: Int by intVarBit("varbit.poh_house_style")
public var Player.pohHouseSize: Int by intVarBit("varbit.poh_house_size")
public var Player.pohBuildingMode: Int by intVarBit("varbit.poh_building_mode")
public var Player.pohServantType: Int by intVarBit("varbit.poh_servant_type")
public var Player.pohServantPay: Int by intVarBit("varbit.poh_servant_pay")
public var Player.pohDoorsOption: Int by intVarBit("varbit.poh_doors_option")

internal fun Player.constructionLevelForHouse(): Int = baseConstructionLvl
Loading