Skip to content
Merged
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
1 change: 1 addition & 0 deletions api/account/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ kotlin {
}

dependencies {
implementation(libs.openrune.central.common)
implementation(projects.api.attr)
implementation(libs.bundles.logging)
implementation(libs.fastutil)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.rsmod.api.account.autosave

import com.github.michaelbull.logging.InlineLogger
import jakarta.inject.Inject
import jakarta.inject.Singleton
import java.util.concurrent.ConcurrentHashMap
import dev.openrune.types.InvScope
import org.rsmod.api.account.AccountManager
import org.rsmod.api.account.saver.request.AccountSaveResponse
import org.rsmod.api.attr.AttributeKey
import org.rsmod.api.attr.AttributeMap
import org.rsmod.game.entity.Player
import org.rsmod.game.entity.PlayerPersistenceHints

/**
* Queues background character saves (same pipeline as logout) on a timer and when important state
* changes. Coalesces overlapping requests per player while a save is already in flight.
*/
@Singleton
public class PlayerAutosaveOrchestrator
@Inject
constructor(
private val accountManager: AccountManager,
) {
private val logger = InlineLogger()

private val queuedSave: MutableSet<Player> =
ConcurrentHashMap.newKeySet()

private var periodicCounter: Int = PERIODIC_INTERVAL_CYCLES

init {
PlayerPersistenceHints.bind(this::onPersistenceRelevantChange)
AttributeMap.persistenceMutationSink = { key ->
onPersistentAttrMutated(key)
}
}

/**
* Run once per game tick after inventory updates (while [InvScope.Perm] inventories may still
* report [org.rsmod.game.inv.Inventory.hasModifiedSlots]).
*/
public fun processEndOfTick(players: Iterable<Player>) {
for (player in players) {
if (!player.canProcess) {
continue
}
if (player.invMap.values.any { it.type.scope == InvScope.Perm && it.hasModifiedSlots() }) {
onPersistenceRelevantChange(player)
}
}
periodicCounter--
if (periodicCounter <= 0) {
periodicCounter = PERIODIC_INTERVAL_CYCLES
for (player in players) {
if (player.canProcess && player.persistenceSaveEligible()) {
requestBackgroundSave(player)
}
}
}
}

private fun onPersistentAttrMutated(@Suppress("UNUSED_PARAMETER") key: AttributeKey<*>) {
val player = PlayerPersistenceHints.activeOrNull() ?: return
onPersistenceRelevantChange(player)
}

private fun onPersistenceRelevantChange(player: Player) {
if (!player.persistenceSaveEligible()) {
return
}
requestBackgroundSave(player)
}

private fun requestBackgroundSave(player: Player) {
if (!player.persistenceSaveEligible()) {
return
}
if (!queuedSave.add(player)) {
return
}
accountManager.save(player) { response ->
queuedSave.remove(player)
when (response) {
is AccountSaveResponse.Success -> {}
is AccountSaveResponse.ExcessiveRetries ->
logger.error { "Background autosave failed after retries for ${player.username}" }
is AccountSaveResponse.InternalShutdownError ->
logger.error { "Background autosave shutdown error for ${player.username}" }
}
}
}

private companion object {
private const val PERIODIC_INTERVAL_CYCLES: Int = 500
}
}

private fun Player.persistenceSaveEligible(): Boolean =
accountId > 0 && characterId > 0 && processedMapClock > 0
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package org.rsmod.api.account.character.inv

import dev.openrune.ServerCacheManager
import dev.openrune.rscm.RSCM
import dev.openrune.rscm.RSCMType
import dev.openrune.types.util.UncheckedType
import kotlin.collections.iterator
import org.rsmod.api.account.character.CharacterDataStage
Expand All @@ -13,12 +10,10 @@ import org.rsmod.game.inv.InvObj
public class CharacterInventoryApplier : CharacterDataStage.Applier<CharacterInventoryData> {
override fun apply(player: Player, data: CharacterInventoryData) {
for (loaded in data.inventories) {
val type = ServerCacheManager.getInventory(loaded.type) ?: return
val inventory = player.invMap.getOrPut(RSCM.getReverseMapping(RSCMType.INV, type.id))
val inventory = player.invMap.getOrPut(loaded.invKey)

for ((slot, obj) in loaded.objs) {
val (type, count, vars) = obj
inventory[slot] = InvObj(type, count, vars = vars)
inventory[slot] = InvObj(obj.objKey, count = obj.count, vars = obj.vars)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import org.rsmod.api.account.character.CharacterDataStage

public class CharacterInventoryData(public val inventories: List<Inventory>) :
CharacterDataStage.Segment {
public data class Obj(val type: Int, val count: Int, val vars: Int)
public data class Obj(val objKey: String, val count: Int, val vars: Int)

public data class Inventory(
val rowId: Int,
val type: Int,
val characterId: Int,
val invDbKey: String,
val invKey: String,
val objs: MutableMap<Int, Obj> = mutableMapOf(),
)
}
Loading
Loading