diff --git a/.data/raw-cache/server/inv.toml b/.data/raw-cache/server/inv.toml index 8ab211cc3..aa7b2c19f 100644 --- a/.data/raw-cache/server/inv.toml +++ b/.data/raw-cache/server/inv.toml @@ -41,3 +41,12 @@ id = "inv.skill_guide_hunting_tracking" inherit = "inv.skill_guide_hunting_tracking" stack = "Never" +[[inventory]] +isServerOnly = true +id = "inv.collection_transmit" +inherit = "inv.collection_transmit" +size = 2000 +scope = "Perm" +stack = "Always" +protect = false + diff --git a/api/net/src/main/kotlin/org/rsmod/api/net/rsprot/handlers/If3ButtonHandler.kt b/api/net/src/main/kotlin/org/rsmod/api/net/rsprot/handlers/If3ButtonHandler.kt index cdd074e68..b3ebb603b 100644 --- a/api/net/src/main/kotlin/org/rsmod/api/net/rsprot/handlers/If3ButtonHandler.kt +++ b/api/net/src/main/kotlin/org/rsmod/api/net/rsprot/handlers/If3ButtonHandler.kt @@ -3,11 +3,15 @@ package org.rsmod.api.net.rsprot.handlers import com.github.michaelbull.logging.InlineLogger import dev.openrune.ServerCacheManager import dev.openrune.definition.type.widget.IfEvent +import dev.openrune.rscm.RSCM +import dev.openrune.rscm.RSCMType import dev.openrune.types.aconverted.interf.IfButtonOp import jakarta.inject.Inject import net.rsprot.protocol.game.incoming.buttons.If3Button import org.rsmod.annotations.InternalApi import org.rsmod.api.net.rsprot.player.InterfaceEvents +import org.rsmod.api.player.debug.componentClickDebug +import org.rsmod.api.player.output.mes import org.rsmod.api.player.protect.ProtectedAccessLauncher import org.rsmod.api.player.ui.IfModalButton import org.rsmod.api.player.ui.IfOverlayButton @@ -48,6 +52,12 @@ constructor(private val eventBus: EventBus, private val protectedAccess: Protect val comsub = message.sub val buttonOp = message.buttonOp + if (player.componentClickDebug) { + val name = RSCM.getReverseMapping(RSCMType.COMPONENT, message.asComponent.packed) + .ifEmpty { "component." } + player.mes("[componentdebug] $name comsub=$comsub op=$buttonOp") + } + val opEnabled = InterfaceEvents.isEnabled(player.ui, componentType, comsub, buttonOp.toIfEvent()) if (!opEnabled) { diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/debug/ComponentClickDebug.kt b/api/player/src/main/kotlin/org/rsmod/api/player/debug/ComponentClickDebug.kt new file mode 100644 index 000000000..15cc88bd6 --- /dev/null +++ b/api/player/src/main/kotlin/org/rsmod/api/player/debug/ComponentClickDebug.kt @@ -0,0 +1,16 @@ +package org.rsmod.api.player.debug + +import org.rsmod.api.attr.AttributeKey +import org.rsmod.game.entity.Player + +private val COMPONENT_CLICK_DEBUG_ATTR = AttributeKey(temp = true) + +/** + * When enabled, [org.rsmod.api.net.rsprot.handlers.If3ButtonHandler] reports every button click + * this player sends to the client (component RSCM name, comsub, and op) + */ +public var Player.componentClickDebug: Boolean + get() = attr[COMPONENT_CLICK_DEBUG_ATTR] == true + set(value) { + attr[COMPONENT_CLICK_DEBUG_ATTR] = value + } diff --git a/content/areas/city/prifddinas/build.gradle.kts b/content/areas/city/prifddinas/build.gradle.kts index e5f484540..fecabb8fb 100644 --- a/content/areas/city/prifddinas/build.gradle.kts +++ b/content/areas/city/prifddinas/build.gradle.kts @@ -6,4 +6,5 @@ dependencies { implementation(projects.api.pluginCommons) implementation(projects.api.dropTable) implementation(projects.api.dropTablePlugin) + implementation(projects.content.interfaces.collectionLog) } diff --git a/content/areas/city/prifddinas/src/main/kotlin/org/rsmod/content/areas/city/prifddinas/locs/ElvenCrystalChestScript.kt b/content/areas/city/prifddinas/src/main/kotlin/org/rsmod/content/areas/city/prifddinas/locs/ElvenCrystalChestScript.kt index ec747fd82..08ede9604 100644 --- a/content/areas/city/prifddinas/src/main/kotlin/org/rsmod/content/areas/city/prifddinas/locs/ElvenCrystalChestScript.kt +++ b/content/areas/city/prifddinas/src/main/kotlin/org/rsmod/content/areas/city/prifddinas/locs/ElvenCrystalChestScript.kt @@ -13,6 +13,7 @@ import org.rsmod.api.repo.loc.LocRepository import org.rsmod.api.repo.obj.ObjRepository import org.rsmod.api.script.onOpLoc1 import org.rsmod.api.script.onOpLoc2 +import org.rsmod.content.interfaces.collectionlog.CollectionLog import org.rsmod.game.loc.BoundLocInfo import org.rsmod.plugin.scripts.PluginScript import org.rsmod.plugin.scripts.ScriptContext @@ -73,6 +74,8 @@ class ElvenCrystalChestScript @Inject constructor( private fun ProtectedAccess.giveDrop(drop: DropRollItem) { if (drop.isNothing || !drop.condition(player)) return val obj = drop.transformObj(player) ?: drop.obj - invAddOrDrop(objRepo, obj, drop.rollCount(random)) + val count = drop.rollCount(random) + CollectionLog.grant(player, obj, count) + invAddOrDrop(objRepo, obj, count) } } diff --git a/content/areas/godwars/build.gradle.kts b/content/areas/godwars/build.gradle.kts index a7e5cb889..ddb66e6df 100644 --- a/content/areas/godwars/build.gradle.kts +++ b/content/areas/godwars/build.gradle.kts @@ -3,5 +3,7 @@ plugins { } dependencies { + implementation(projects.api.areaChecker) implementation(projects.api.pluginCommons) + implementation(projects.engine.utilsBits) } diff --git a/content/areas/godwars/src/main/kotlin/org/rsmod/content/areas/godwars/GodwarsModule.kt b/content/areas/godwars/src/main/kotlin/org/rsmod/content/areas/godwars/GodwarsModule.kt index de9ba73d1..5b420ec7a 100644 --- a/content/areas/godwars/src/main/kotlin/org/rsmod/content/areas/godwars/GodwarsModule.kt +++ b/content/areas/godwars/src/main/kotlin/org/rsmod/content/areas/godwars/GodwarsModule.kt @@ -1,14 +1,18 @@ package org.rsmod.content.areas.godwars +import dev.openrune.ServerCacheManager import dev.openrune.rscm.RSCM.asRSCM import dev.openrune.rscm.RSCMType import jakarta.inject.Inject +import org.rsmod.api.area.checker.AreaChecker import org.rsmod.api.death.NpcDeathKillContext import org.rsmod.api.death.NpcDeathKillHook import org.rsmod.api.player.vars.VarPlayerIntMapSetter import org.rsmod.game.entity.Npc import org.rsmod.game.entity.Player +import dev.openrune.types.varp.bits import org.rsmod.plugin.module.PluginModule +import org.rsmod.utils.bits.bitMask public class GodwarsModule : PluginModule() { override fun bind() { @@ -16,16 +20,21 @@ public class GodwarsModule : PluginModule() { } } -internal class GodwarsKillCountHook @Inject constructor() : NpcDeathKillHook { +internal class GodwarsKillCountHook @Inject constructor(private val areas: AreaChecker) : + NpcDeathKillHook { override fun onKill(context: NpcDeathKillContext) { val player = context.hero val npc = context.npc + if (!areas.inArea("area.godwars_dungeon", player.coords)) return val faction = FACTIONS.firstOrNull { it.matches(npc) } ?: return player.increment(faction.counterVarbit) } - private fun Player.increment(varp: String) { - VarPlayerIntMapSetter.set(this, varp, vars[varp] + 1) + private fun Player.increment(varbit: String) { + val next = vars[varbit] + 1 + val type = ServerCacheManager.getVarbit(varbit.asRSCM(RSCMType.VARBIT)) + val max = type?.bits?.bitMask ?: Int.MAX_VALUE.toLong() + VarPlayerIntMapSetter.set(this, varbit, next.coerceAtMost(max.toInt())) } private class Faction( diff --git a/content/areas/wilderness/build.gradle.kts b/content/areas/wilderness/build.gradle.kts index 3b5b4f0b4..78e9f3692 100644 --- a/content/areas/wilderness/build.gradle.kts +++ b/content/areas/wilderness/build.gradle.kts @@ -11,4 +11,5 @@ dependencies { implementation(projects.api.attr) implementation(projects.api.dropTable) implementation(projects.api.dropTablePlugin) + implementation(projects.content.interfaces.collectionLog) } diff --git a/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/LarransChestScript.kt b/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/LarransChestScript.kt index 34a7d0bf6..9baaab3b7 100644 --- a/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/LarransChestScript.kt +++ b/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/LarransChestScript.kt @@ -13,6 +13,7 @@ import org.rsmod.api.repo.loc.LocRepository import org.rsmod.api.repo.obj.ObjRepository import org.rsmod.api.script.onOpLoc1 import org.rsmod.api.script.onOpLoc2 +import org.rsmod.content.interfaces.collectionlog.CollectionLog import org.rsmod.game.loc.BoundLocInfo import org.rsmod.plugin.scripts.PluginScript import org.rsmod.plugin.scripts.ScriptContext @@ -78,6 +79,8 @@ class LarransChestScript @Inject constructor( private fun ProtectedAccess.giveDrop(drop: DropRollItem) { if (drop.isNothing || !drop.condition(player)) return val obj = drop.transformObj(player) ?: drop.obj - invAddOrDrop(objRepo, obj, drop.rollCount(random)) + val count = drop.rollCount(random) + CollectionLog.grant(player, obj, count) + invAddOrDrop(objRepo, obj, count) } } diff --git a/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/ZombiePirateLockerScript.kt b/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/ZombiePirateLockerScript.kt index 28bf85cb7..ea091b2c0 100644 --- a/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/ZombiePirateLockerScript.kt +++ b/content/areas/wilderness/src/main/kotlin/org/rsmod/content/areas/wilderness/locs/ZombiePirateLockerScript.kt @@ -14,6 +14,7 @@ import org.rsmod.api.repo.loc.LocRepository import org.rsmod.api.repo.obj.ObjRepository import org.rsmod.api.script.onOpHeld1 import org.rsmod.api.script.onOpLoc1 +import org.rsmod.content.interfaces.collectionlog.CollectionLog import org.rsmod.game.inv.isType import org.rsmod.game.loc.BoundLocInfo import org.rsmod.plugin.scripts.PluginScript @@ -71,7 +72,9 @@ constructor( if (player.front.isType("obj.wild_cave_amulet")) { obj = ocCert(obj).internalName } - invAddOrDrop(objRepo, obj, drop.rollCount(random)) + val count = drop.rollCount(random) + CollectionLog.grant(player, obj, count) + invAddOrDrop(objRepo, obj, count) } private suspend fun ProtectedAccess.unlockBlipScroll() { diff --git a/content/areas/zeah/build.gradle.kts b/content/areas/zeah/build.gradle.kts index e5f484540..fecabb8fb 100644 --- a/content/areas/zeah/build.gradle.kts +++ b/content/areas/zeah/build.gradle.kts @@ -6,4 +6,5 @@ dependencies { implementation(projects.api.pluginCommons) implementation(projects.api.dropTable) implementation(projects.api.dropTablePlugin) + implementation(projects.content.interfaces.collectionLog) } diff --git a/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/BrimstoneChestScript.kt b/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/BrimstoneChestScript.kt index 6e1bf28fc..ed2f40503 100644 --- a/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/BrimstoneChestScript.kt +++ b/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/BrimstoneChestScript.kt @@ -13,6 +13,7 @@ import org.rsmod.api.repo.loc.LocRepository import org.rsmod.api.repo.obj.ObjRepository import org.rsmod.api.script.onOpLoc1 import org.rsmod.api.script.onOpLoc2 +import org.rsmod.content.interfaces.collectionlog.CollectionLog import org.rsmod.game.loc.BoundLocInfo import org.rsmod.plugin.scripts.PluginScript import org.rsmod.plugin.scripts.ScriptContext @@ -68,6 +69,8 @@ class BrimstoneChestScript @Inject constructor( private fun ProtectedAccess.giveDrop(drop: DropRollItem) { if (drop.isNothing || !drop.condition(player)) return val obj = drop.transformObj(player) ?: drop.obj - invAddOrDrop(objRepo, obj, drop.rollCount(random)) + val count = drop.rollCount(random) + CollectionLog.grant(player, obj, count) + invAddOrDrop(objRepo, obj, count) } } diff --git a/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/GrubbyChestScript.kt b/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/GrubbyChestScript.kt index 3ef54054b..c29632ff2 100644 --- a/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/GrubbyChestScript.kt +++ b/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/GrubbyChestScript.kt @@ -12,6 +12,7 @@ import org.rsmod.api.player.vars.VarPlayerIntMapSetter import org.rsmod.api.repo.obj.ObjRepository import org.rsmod.api.script.onOpLoc1 import org.rsmod.api.script.onOpLoc2 +import org.rsmod.content.interfaces.collectionlog.CollectionLog import org.rsmod.plugin.scripts.PluginScript import org.rsmod.plugin.scripts.ScriptContext @@ -65,6 +66,8 @@ class GrubbyChestScript @Inject constructor( private fun ProtectedAccess.giveDrop(drop: DropRollItem) { if (drop.isNothing || !drop.condition(player)) return val obj = drop.transformObj(player) ?: drop.obj - invAddOrDrop(objRepo, obj, drop.rollCount(random)) + val count = drop.rollCount(random) + CollectionLog.grant(player, obj, count) + invAddOrDrop(objRepo, obj, count) } } diff --git a/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/MoonChestScript.kt b/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/MoonChestScript.kt index 232071c79..c2fd91c39 100644 --- a/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/MoonChestScript.kt +++ b/content/areas/zeah/src/main/kotlin/org/rsmod/content/areas/zeah/locs/MoonChestScript.kt @@ -13,6 +13,7 @@ import org.rsmod.api.repo.loc.LocRepository import org.rsmod.api.repo.obj.ObjRepository import org.rsmod.api.script.onOpHeldU import org.rsmod.api.script.onOpLoc1 +import org.rsmod.content.interfaces.collectionlog.CollectionLog import org.rsmod.game.loc.BoundLocInfo import org.rsmod.plugin.scripts.PluginScript import org.rsmod.plugin.scripts.ScriptContext @@ -67,7 +68,9 @@ constructor( private fun ProtectedAccess.giveDrop(drop: DropRollItem) { if (drop.isNothing || !drop.condition(player)) return val obj = drop.transformObj(player) ?: drop.obj - invAddOrDrop(objRepo, obj, drop.rollCount(random)) + val count = drop.rollCount(random) + CollectionLog.grant(player, obj, count) + invAddOrDrop(objRepo, obj, count) } private fun ProtectedAccess.combineKeyHalves() { diff --git a/content/drops/build.gradle.kts b/content/drops/build.gradle.kts index 97a191c3b..d4592cb72 100644 --- a/content/drops/build.gradle.kts +++ b/content/drops/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(projects.api.playerOutput) implementation(projects.api.random) implementation(projects.api.repo) + implementation(projects.content.interfaces.collectionLog) implementation(projects.content.skills.slayer) implementation(projects.content.quest) implementation(projects.engine.game) diff --git a/content/drops/src/main/kotlin/org/rsmod/content/drops/NpcDropTableKillHook.kt b/content/drops/src/main/kotlin/org/rsmod/content/drops/NpcDropTableKillHook.kt index e289d951f..4fda351b5 100644 --- a/content/drops/src/main/kotlin/org/rsmod/content/drops/NpcDropTableKillHook.kt +++ b/content/drops/src/main/kotlin/org/rsmod/content/drops/NpcDropTableKillHook.kt @@ -17,6 +17,7 @@ import org.rsmod.api.droptable.rollCount import org.rsmod.api.player.output.ClientScripts import org.rsmod.api.random.GameRandom import org.rsmod.api.repo.obj.ObjRepository +import org.rsmod.content.interfaces.collectionlog.CollectionLog import org.rsmod.game.entity.Npc import org.rsmod.game.entity.Player import org.rsmod.map.CoordGrid @@ -88,6 +89,7 @@ constructor( } val obj = drop.transformObj(receiver) ?: drop.obj val count = drop.rollCount(random) + CollectionLog.grant(receiver, obj, count) val spawned = objRepo.add(obj, coords, duration, receiver, count) ClientScripts.lootTrackerAddLoot(receiver, npc.id, eventId, spawned.type, spawned.count) for (bonus in drop.bonusDrops) { diff --git a/content/interfaces/collection-log/build.gradle.kts b/content/interfaces/collection-log/build.gradle.kts new file mode 100644 index 000000000..594ca7de9 --- /dev/null +++ b/content/interfaces/collection-log/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("base-conventions") +} + +dependencies { + implementation(projects.api.pluginCommons) + implementation(projects.engine.utilsBits) +} diff --git a/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLog.kt b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLog.kt new file mode 100644 index 000000000..9cc5ccaef --- /dev/null +++ b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLog.kt @@ -0,0 +1,38 @@ +package org.rsmod.content.interfaces.collectionlog + +import dev.openrune.rscm.RSCM.asRSCM +import dev.openrune.rscm.RSCMType +import org.rsmod.api.invtx.invAdd +import org.rsmod.api.player.vars.VarPlayerIntMapSetter +import org.rsmod.game.entity.Player + +public object CollectionLog { + /** Central entry point for marking an item as obtained for the collection log */ + public fun grant(player: Player, obj: String, count: Int = 1) { + grant(player, obj.asRSCM(RSCMType.OBJ), count) + } + + public fun grant(player: Player, objId: Int, count: Int = 1) { + if (count <= 0) { + return + } + if (!CollectionLogItems.contains(objId)) { + return + } + player.invAdd(player.collectionTransmit, objId, count) + checkCategoryCompletion(player, objId) + } + + private fun checkCategoryCompletion(player: Player, itemId: Int) { + val inv = player.collectionTransmit + for (category in CollectionLogCategories.categoriesContaining(itemId)) { + if (player.vars[category.completedVarbit] != 0) { + continue + } + val allObtained = category.itemIds.all { inv.countOf(it) > 0 } + if (allObtained) { + VarPlayerIntMapSetter.set(player, category.completedVarbit, 1) + } + } + } +} diff --git a/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogCategories.kt b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogCategories.kt new file mode 100644 index 000000000..686da52a7 --- /dev/null +++ b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogCategories.kt @@ -0,0 +1,93 @@ +package org.rsmod.content.interfaces.collectionlog + +import dev.openrune.ServerCacheManager +import dev.openrune.definition.type.VarBitType +import dev.openrune.types.enums.enum +import dev.openrune.types.varp.VarpServerType +import org.rsmod.api.table.CollectionLogCategoriesRow +import org.rsmod.game.entity.Player + +internal data class CollectionLogCategory( + val structId: Int, + val completedVarbit: VarBitType, + val itemIds: Set, + val countVarps: List, +) + +internal object CollectionLogCategories { + + private const val TAB_STRUCT_BASE = 471 + private const val TAB_COUNT = 5 + private const val TAB_CATEGORY_LIST_PARAM = 683 + + private val structIdToTabComsub: Map> by lazy { + val map = mutableMapOf>() + for (tab in 0 until TAB_COUNT) { + val tabStruct = ServerCacheManager.getStruct(TAB_STRUCT_BASE + tab) ?: continue + val enumId = tabStruct.params?.get(TAB_CATEGORY_LIST_PARAM) as? Int ?: continue + val categoryStructIds = enum(enumId).backing.toSortedMap().values + categoryStructIds.filterNotNull().forEachIndexed { comsub, structId -> + map[structId] = tab to comsub + } + } + map + } + + internal val allCategoryStructIds: Set by lazy { structIdToTabComsub.keys } + + private val allCategories: List by lazy { + CollectionLogCategoriesRow.all().mapNotNull(::resolve) + } + + private val itemIdToCategories: Map> by lazy { + val map = mutableMapOf>() + for (category in allCategories) { + for (itemId in category.itemIds) { + map.getOrPut(itemId) { mutableListOf() }.add(category) + } + } + map + } + + private val categoryByTabAndComsub: Map, CollectionLogCategory> by lazy { + allCategories + .mapNotNull { category -> structIdToTabComsub[category.structId]?.let { it to category } } + .toMap() + } + + private val tabItemIds: Map> by lazy { + allCategories + .groupBy { structIdToTabComsub[it.structId]?.first } + .mapNotNull { (tab, categories) -> + tab?.let { it to categories.flatMap { c -> c.itemIds }.distinct() } + } + .toMap() + } + + fun categoriesContaining(itemId: Int): List = + itemIdToCategories[itemId] ?: emptyList() + + fun forCategory(tab: Int, comsub: Int): CollectionLogCategory? = + categoryByTabAndComsub[tab to comsub] + + fun tabTotalCount(tab: Int): Int = tabItemIds[tab]?.size ?: 0 + + fun tabObtainedCount(player: Player, tab: Int): Int { + val inv = player.collectionTransmit + return tabItemIds[tab]?.count { inv.countOf(it) > 0 } ?: 0 + } + + private fun resolve(row: CollectionLogCategoriesRow): CollectionLogCategory? { + if (row.structId !in structIdToTabComsub) return null + val completedVarbit = ServerCacheManager.getVarbit(row.completedVarbit) ?: return null + val itemIds = CollectionLogItems.itemsInCategoryStruct(row.structId).toSet() + if (itemIds.isEmpty()) { + return null + } + val countVarps = + listOfNotNull(row.countVarp1, row.countVarp2, row.countVarp3).mapNotNull { varpId -> + ServerCacheManager.getVarp(varpId) + } + return CollectionLogCategory(row.structId, completedVarbit, itemIds, countVarps) + } +} diff --git a/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogItems.kt b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogItems.kt new file mode 100644 index 000000000..4c28bbf17 --- /dev/null +++ b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogItems.kt @@ -0,0 +1,48 @@ +package org.rsmod.content.interfaces.collectionlog + +import dev.openrune.ServerCacheManager +import dev.openrune.types.ItemServerType +import dev.openrune.types.enums.enum +import org.rsmod.api.player.vars.VarPlayerIntMapSetter +import org.rsmod.game.entity.Player +import org.rsmod.game.inv.Inventory + +/** The collection log tracks obtained items directly in this inventory rather than varbits. */ +public val Player.collectionTransmit: Inventory + get() = invMap.getOrPut("inv.collection_transmit") + +internal fun Inventory.countOf(objId: Int): Int = objs.firstOrNull { it?.id == objId }?.count ?: 0 + +internal object CollectionLogItems { + private const val ITEMS_ENUM_PARAM = 690 + + internal fun itemsInCategoryStruct(structId: Int): List { + val struct = ServerCacheManager.getStruct(structId) ?: return emptyList() + val enumId = struct.params?.get(ITEMS_ENUM_PARAM) as? Int ?: return emptyList() + return enum(enumId).backing.values.filterNotNull().map { it.id } + } + + /** Every item obj id that appears in any collection log category. */ + private val allItemIds: Set by lazy { + CollectionLogCategories.allCategoryStructIds + .asSequence() + .flatMap { itemsInCategoryStruct(it).asSequence() } + .toSet() + } + + val totalCount: Int by lazy { allItemIds.size } + + fun contains(objId: Int): Boolean = objId in allItemIds + + fun obtainedCount(player: Player): Int { + val inv = player.collectionTransmit + return allItemIds.count { inv.countOf(it) > 0 } + } +} + +public fun Player.applyCollectionCount() { + val obtained = CollectionLogItems.obtainedCount(this) + VarPlayerIntMapSetter.set(this, "varp.collection_count", obtained) + VarPlayerIntMapSetter.set(this, "varp.collection_count_max", CollectionLogItems.totalCount) + VarPlayerIntMapSetter.set(this, "varp.collection_count_highscores", obtained) +} diff --git a/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogScript.kt b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogScript.kt new file mode 100644 index 000000000..07a01ced8 --- /dev/null +++ b/content/interfaces/collection-log/src/main/kotlin/org/rsmod/content/interfaces/collectionlog/CollectionLogScript.kt @@ -0,0 +1,298 @@ +package org.rsmod.content.interfaces.collectionlog + +import dev.openrune.definition.type.widget.IfEvent +import dev.openrune.rscm.RSCM.asRSCM +import dev.openrune.rscm.RSCMType +import jakarta.inject.Inject +import org.rsmod.api.player.output.runClientScript +import org.rsmod.api.player.startInvTransmit +import org.rsmod.api.player.stopInvTransmit +import org.rsmod.api.player.ui.ifCloseSub +import org.rsmod.api.player.ui.ifOpenOverlay +import org.rsmod.api.player.ui.ifSetEvents +import org.rsmod.api.player.vars.VarPlayerIntMapSetter +import org.rsmod.api.player.vars.intVarBit +import org.rsmod.api.script.onIfClose +import org.rsmod.api.script.onIfOpen +import org.rsmod.api.script.onIfOverlayButton +import org.rsmod.events.EventBus +import org.rsmod.game.entity.Player +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +class CollectionLogScript @Inject constructor(private val eventBus: EventBus) : PluginScript() { + private var Player.lastTab: Int by intVarBit("varbit.collection_last_tab") + private var Player.lastCategory: Int by intVarBit("varbit.collection_last_category") + + override fun ScriptContext.startup() { + onIfOpen("interface.collection") { + player.startInvTransmit(player.collectionTransmit) + player.registerCategoryRowEvents() + player.registerBurgerMenuEvents("component.collection:burger_menu_frame") + player.drawCollectionLog( + player.lastTab.coerceIn(BOSS_TAB, OTHER_TAB), + player.lastCategory.coerceAtLeast(0), + ) + } + onIfClose("interface.collection") { player.stopInvTransmit(player.collectionTransmit) } + + onIfOpen("interface.collection_overview") { + player.registerOverviewSubsectionEvents() + player.registerBurgerMenuEvents("component.collection_overview:burger_menu_frame") + player.drawCollectionOverview() + } + + onIfOverlayButton("component.collection:boss_tab") { player.switchTab(BOSS_TAB) } + onIfOverlayButton("component.collection:raid_tab") { player.switchTab(RAID_TAB) } + onIfOverlayButton("component.collection:clue_tab") { player.switchTab(CLUE_TAB) } + onIfOverlayButton("component.collection:minigame_tab") { player.switchTab(MINIGAME_TAB) } + onIfOverlayButton("component.collection:other_tab") { player.switchTab(OTHER_TAB) } + onIfOverlayButton("component.collection:close") { player.closeCollectionLog() } + onIfOverlayButton("component.collection_overview:close") { + player.closeCollectionOverview() + } + + onIfOverlayButton("component.collection:boss_background") { + player.selectCategory(BOSS_TAB, it.comsub) + } + onIfOverlayButton("component.collection:raid_background") { + player.selectCategory(RAID_TAB, it.comsub) + } + onIfOverlayButton("component.collection:clue_background") { + player.selectCategory(CLUE_TAB, it.comsub) + } + onIfOverlayButton("component.collection:minigame_background") { + player.selectCategory(MINIGAME_TAB, it.comsub) + } + onIfOverlayButton("component.collection:other_background") { + player.selectCategory(OTHER_TAB, it.comsub) + } + + onIfOverlayButton("component.collection_overview:subsection_buttons_click") { + player.openCategoryFromOverview(it.comsub) + } + + onIfOverlayButton("component.collection:burger_menu_frame") { + player.handleLogBurgerMenuClick(it.comsub) + } + onIfOverlayButton("component.collection_overview:burger_menu_frame") { + player.handleOverviewBurgerMenuClick(it.comsub) + } + } + + private fun Player.registerCategoryRowEvents() { + val range = 0..MAX_CATEGORY_ROWS + ifSetEvents("component.collection:boss_background", range, IfEvent.Op1) + ifSetEvents("component.collection:raid_background", range, IfEvent.Op1) + ifSetEvents("component.collection:clue_background", range, IfEvent.Op1) + ifSetEvents("component.collection:minigame_background", range, IfEvent.Op1) + ifSetEvents("component.collection:other_background", range, IfEvent.Op1) + } + + private fun Player.selectCategory(tab: Int, category: Int) { + lastTab = tab + lastCategory = category + drawCollectionLog(tab, category) + } + + private fun Player.registerOverviewSubsectionEvents() { + ifSetEvents( + "component.collection_overview:subsection_buttons_click", + BOSS_TAB..OTHER_TAB, + IfEvent.Op1, + ) + } + + private fun Player.openCategoryFromOverview(tab: Int) { + lastTab = tab + lastCategory = 0 + openLogFromOverview() + } + + private fun Player.registerBurgerMenuEvents(component: String) { + ifSetEvents(component, BURGER_VIEW_LOG_ROW..BURGER_VIEW_OVERVIEW_ROW, IfEvent.Op1) + } + + private fun Player.handleLogBurgerMenuClick(comsub: Int) { + if (comsub == BURGER_VIEW_OVERVIEW_ROW) { + openOverviewFromLog() + } + } + + private fun Player.handleOverviewBurgerMenuClick(comsub: Int) { + if (comsub == BURGER_VIEW_LOG_ROW) { + openLogFromOverview() + } + } + + private fun Player.openLogFromOverview() { + closeCollectionOverview() + ifOpenOverlay("interface.collection", eventBus) + } + + private fun Player.openOverviewFromLog() { + closeCollectionLog() + ifOpenOverlay("interface.collection_overview", eventBus) + } + + private fun Player.closeCollectionLog() { + ifCloseSub("interface.collection", eventBus) + } + + private fun Player.closeCollectionOverview() { + ifCloseSub("interface.collection_overview", eventBus) + } + + private fun Player.switchTab(tab: Int) { + lastTab = tab + lastCategory = 0 + drawCollectionLog(tab, 0) + } + + private fun Player.drawCollectionLog(tab: Int, category: Int) { + applyCollectionCount() + applyCategoryCounts(tab, category) + val components = TAB_COMPONENTS.getValue(tab) + runClientScript( + "clientscript.[clientscript,collection_draw]".asRSCM(RSCMType.CLIENTSCRIPT), + tab, + components.container, + components.background, + components.text, + components.scrollbar, + TAB_STRUCT_BASE + tab, + category, + ) + } + + private fun Player.drawCollectionOverview() { + applyCollectionCount() + applyTabCounts() + runClientScript( + "clientscript.[clientscript,collection_overview_draw]".asRSCM(RSCMType.CLIENTSCRIPT), + "component.collection_overview:subsection_general_content".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:subsection_buttons".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:subsection_buttons_click".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:subsection_progress".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:progress_bar".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:progress_bar_rank_left".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:progress_bar_rank_right".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:progress_left_text".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:progress_right_text".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:latest_items_data".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:burger_btn_menu".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:burger_menu_frame".asRSCM(RSCMType.COMPONENT), + "component.collection_overview:burger_menu_overlay".asRSCM(RSCMType.COMPONENT), + ) + } + + private fun Player.applyTabCounts() { + for (tab in TAB_COUNT_VARPS.indices) { + val (countVarp, maxVarp) = TAB_COUNT_VARPS[tab] + VarPlayerIntMapSetter.set( + this, + countVarp, + CollectionLogCategories.tabObtainedCount(this, tab), + ) + VarPlayerIntMapSetter.set(this, maxVarp, CollectionLogCategories.tabTotalCount(tab)) + } + } + + private fun Player.applyCategoryCounts(tab: Int, comsub: Int) { + val countVarps = CollectionLogCategories.forCategory(tab, comsub)?.countVarps.orEmpty() + for (i in COUNT_TARGET_VARPS.indices) { + val value = countVarps.getOrNull(i)?.let { vars[it] } ?: 0 + VarPlayerIntMapSetter.set(this, COUNT_TARGET_VARPS[i], value) + } + // Placeholder until we have system that saves PBs + for (varp in PB_TARGET_VARPS) { + VarPlayerIntMapSetter.set(this, varp, 0) + } + } + + private class TabComponents( + container: String, + background: String, + text: String, + scrollbar: String, + ) { + val container: Int = container.asRSCM(RSCMType.COMPONENT) + val background: Int = background.asRSCM(RSCMType.COMPONENT) + val text: Int = text.asRSCM(RSCMType.COMPONENT) + val scrollbar: Int = scrollbar.asRSCM(RSCMType.COMPONENT) + } + + private companion object { + const val BOSS_TAB = 0 + const val RAID_TAB = 1 + const val CLUE_TAB = 2 + const val MINIGAME_TAB = 3 + const val OTHER_TAB = 4 + const val TAB_STRUCT_BASE = 471 + const val MAX_CATEGORY_ROWS = 300 + + const val BURGER_VIEW_LOG_ROW = 10 + + const val BURGER_VIEW_OVERVIEW_ROW = 12 + + val COUNT_TARGET_VARPS = + listOf( + "varp.collection_category_count", + "varp.collection_category_count2", + "varp.collection_category_count3", + ) + val PB_TARGET_VARPS = + listOf( + "varp.collection_personal_best_transmit", + "varp.collection_personal_best_transmit_2", + ) + + val TAB_COUNT_VARPS = + listOf( + "varp.collection_count_bosses" to "varp.collection_count_bosses_max", + "varp.collection_count_raids" to "varp.collection_count_raids_max", + "varp.collection_count_clues" to "varp.collection_count_clues_max", + "varp.collection_count_minigames" to "varp.collection_count_minigames_max", + "varp.collection_count_other" to "varp.collection_count_other_max", + ) + + val TAB_COMPONENTS = + mapOf( + BOSS_TAB to + TabComponents( + "component.collection:boss_container", + "component.collection:boss_background", + "component.collection:boss_text", + "component.collection:boss_scrollbar", + ), + RAID_TAB to + TabComponents( + "component.collection:raid_container", + "component.collection:raid_background", + "component.collection:raid_text", + "component.collection:raid_scrollbar", + ), + CLUE_TAB to + TabComponents( + "component.collection:clue_container", + "component.collection:clue_background", + "component.collection:clue_text", + "component.collection:clue_scrollbar", + ), + MINIGAME_TAB to + TabComponents( + "component.collection:minigame_container", + "component.collection:minigame_background", + "component.collection:minigame_text", + "component.collection:minigame_scrollbar", + ), + OTHER_TAB to + TabComponents( + "component.collection:other_container", + "component.collection:other_background", + "component.collection:other_text", + "component.collection:other_scrollbar", + ), + ) + } +} diff --git a/content/interfaces/collection-log/src/main/resources/gamevals.toml b/content/interfaces/collection-log/src/main/resources/gamevals.toml new file mode 100644 index 000000000..6e919d86d --- /dev/null +++ b/content/interfaces/collection-log/src/main/resources/gamevals.toml @@ -0,0 +1,128 @@ +[gamevals.dbtable] +collection_log_categories=65507 + +[gamevals.dbrow] +collection_log_category_abyssal_sire=60123 +collection_log_category_alchemical_hydra=60122 +collection_log_category_amoxliatl=60121 +collection_log_category_araxxor=60120 +collection_log_category_barrows_chests=60119 +collection_log_category_brutus=60118 +collection_log_category_bryophyta=60117 +collection_log_category_callisto_and_artio=60116 +collection_log_category_cerberus=60115 +collection_log_category_chaos_elemental=60114 +collection_log_category_chaos_fanatic=60113 +collection_log_category_commander_zilyana=60112 +collection_log_category_corporeal_beast=60111 +collection_log_category_crazy_archaeologist=60110 +collection_log_category_dagannoth_kings=60109 +collection_log_category_deranged_archaeologist=60108 +collection_log_category_doom_of_mokhaiotl=60107 +collection_log_category_duke_sucellus=60106 +collection_log_category_the_fight_caves=60105 +collection_log_category_fortis_colosseum=60104 +collection_log_category_the_gauntlet=60103 +collection_log_category_general_graardor=60102 +collection_log_category_giant_mole=60101 +collection_log_category_grotesque_guardians=60100 +collection_log_category_hespori=60099 +collection_log_category_the_hueycoatl=60098 +collection_log_category_the_inferno=60097 +collection_log_category_kalphite_queen=60096 +collection_log_category_king_black_dragon=60095 +collection_log_category_kraken=60094 +collection_log_category_kree_arra=60093 +collection_log_category_k_ril_tsutsaroth=60092 +collection_log_category_the_leviathan=60091 +collection_log_category_the_mad_angel=60090 +collection_log_category_maggot_king=60089 +collection_log_category_moons_of_peril=60088 +collection_log_category_nex=60087 +collection_log_category_the_nightmare=60086 +collection_log_category_obor=60085 +collection_log_category_phantom_muspah=60084 +collection_log_category_royal_titans=60083 +collection_log_category_sarachnis=60082 +collection_log_category_scorpia=60081 +collection_log_category_scurrius=60080 +collection_log_category_shellbane_gryphon=60079 +collection_log_category_skotizo=60078 +collection_log_category_tempoross=60077 +collection_log_category_thermonuclear_smoke_devil=60076 +collection_log_category_vardorvis=60075 +collection_log_category_venenatis_and_spindel=60074 +collection_log_category_vet_ion_and_calvar_ion=60073 +collection_log_category_vorkath=60072 +collection_log_category_the_whisperer=60071 +collection_log_category_wintertodt=60070 +collection_log_category_yama=60069 +collection_log_category_zalcano=60068 +collection_log_category_zulrah=60067 +collection_log_category_chambers_of_xeric=60066 +collection_log_category_theatre_of_blood=60065 +collection_log_category_tombs_of_amascut=60064 +collection_log_category_beginner_treasure_trails=60063 +collection_log_category_easy_treasure_trails=60062 +collection_log_category_medium_treasure_trails=60061 +collection_log_category_hard_treasure_trails=60060 +collection_log_category_elite_treasure_trails=60059 +collection_log_category_master_treasure_trails=60058 +collection_log_category_hard_treasure_trails_rare=60057 +collection_log_category_elite_treasure_trails_rare=60056 +collection_log_category_master_treasure_trails_rare=60055 +collection_log_category_shared_treasure_trail_rewards=60054 +collection_log_category_scroll_cases=60053 +collection_log_category_barbarian_assault=60052 +collection_log_category_barracuda_trials=60051 +collection_log_category_brimhaven_agility_arena=60050 +collection_log_category_castle_wars=60049 +collection_log_category_fishing_trawler=60048 +collection_log_category_giants_foundry=60047 +collection_log_category_gnome_restaurant=60046 +collection_log_category_guardians_of_the_rift=60045 +collection_log_category_hallowed_sepulchre=60044 +collection_log_category_last_man_standing=60043 +collection_log_category_magic_training_arena=60042 +collection_log_category_mahogany_homes=60041 +collection_log_category_mastering_mixology=60040 +collection_log_category_pest_control=60039 +collection_log_category_rogues_den=60038 +collection_log_category_shades_of_mort_ton=60037 +collection_log_category_soul_wars=60036 +collection_log_category_temple_trekking=60035 +collection_log_category_tithe_farm=60034 +collection_log_category_trouble_brewing=60033 +collection_log_category_vale_totems=60032 +collection_log_category_volcanic_mine=60031 +collection_log_category_aerial_fishing=60030 +collection_log_category_all_pets=60029 +collection_log_category_boat_paints=60028 +collection_log_category_camdozaal=60027 +collection_log_category_champion_s_challenge=60026 +collection_log_category_chompy_bird_hunting=60025 +collection_log_category_colossal_wyrm_agility=60024 +collection_log_category_creature_creation=60023 +collection_log_category_cyclopes=60022 +collection_log_category_elder_chaos_druids=60021 +collection_log_category_forestry=60020 +collection_log_category_fossil_island_notes=60019 +collection_log_category_glough_s_experiments=60018 +collection_log_category_hunter_guild=60017 +collection_log_category_lost_schematics=60016 +collection_log_category_monkey_backpacks=60015 +collection_log_category_motherlode_mine=60014 +collection_log_category_my_notes=60013 +collection_log_category_ocean_encounters=60012 +collection_log_category_random_events=60011 +collection_log_category_revenants=60010 +collection_log_category_rooftop_agility=60009 +collection_log_category_sailing_miscellaneous=60008 +collection_log_category_sea_treasures=60007 +collection_log_category_shayzien_armour=60006 +collection_log_category_shooting_stars=60005 +collection_log_category_skilling_pets=60004 +collection_log_category_slayer=60003 +collection_log_category_tormented_demons=60002 +collection_log_category_tzhaar=60001 +collection_log_category_miscellaneous=60000 diff --git a/content/interfaces/journal-tab/build.gradle.kts b/content/interfaces/journal-tab/build.gradle.kts index a7e5cb889..23d6f1408 100644 --- a/content/interfaces/journal-tab/build.gradle.kts +++ b/content/interfaces/journal-tab/build.gradle.kts @@ -4,4 +4,5 @@ plugins { dependencies { implementation(projects.api.pluginCommons) + implementation(projects.content.interfaces.collectionLog) } diff --git a/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/PlayerExtensions.kt b/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/PlayerExtensions.kt index a1097bf08..62519e126 100644 --- a/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/PlayerExtensions.kt +++ b/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/PlayerExtensions.kt @@ -62,6 +62,7 @@ internal fun Player.prepareJournalTab(tab: SideJournalTab) = } internal fun Player.prepareSummaryTab() { + resyncVar("varp.collection_count_highscores") resyncVar("varp.collection_count_other_max") resyncVar("varp.collection_count_other") resyncVar("varp.collection_count_minigames_max") diff --git a/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/scripts/SummarySideScript.kt b/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/scripts/SummarySideScript.kt index cac688fce..748552642 100644 --- a/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/scripts/SummarySideScript.kt +++ b/content/interfaces/journal-tab/src/main/kotlin/org/rsmod/content/interfaces/journal/tab/scripts/SummarySideScript.kt @@ -1,6 +1,7 @@ package org.rsmod.content.interfaces.journal.tab.scripts import dev.openrune.definition.type.widget.IfEvent +import dev.openrune.types.aconverted.interf.IfButtonOp import jakarta.inject.Inject import org.rsmod.api.player.output.mes import org.rsmod.api.player.protect.ProtectedAccess @@ -11,6 +12,7 @@ import org.rsmod.api.player.ui.ifSetEvents import org.rsmod.api.player.vars.boolVarBit import org.rsmod.api.script.onIfOpen import org.rsmod.api.script.onIfOverlayButton +import org.rsmod.content.interfaces.collectionlog.applyCollectionCount import org.rsmod.content.interfaces.journal.tab.SideJournalTab import org.rsmod.content.interfaces.journal.tab.switchJournalTab import org.rsmod.content.interfaces.journal.tab.updateSummaryTimePlayed @@ -30,11 +32,12 @@ constructor(private val eventBus: EventBus, private val protectedAccess: Protect override fun ScriptContext.startup() { onIfOpen("interface.account_summary_sidepanel") { player.onSummarySideOpen() } onIfOverlayButton("component.account_summary_sidepanel:summary_click_layer") { - player.clickSummaryLayer(it.comsub) + player.clickSummaryLayer(it.comsub, it.op) } } private fun Player.onSummarySideOpen() { + applyCollectionCount() ifSetEvents( "component.account_summary_sidepanel:summary_click_layer", 3..7, @@ -45,12 +48,12 @@ constructor(private val eventBus: EventBus, private val protectedAccess: Protect ) } - private fun Player.clickSummaryLayer(comsub: Int) { + private fun Player.clickSummaryLayer(comsub: Int, op: IfButtonOp) { when (comsub) { 3 -> clickQuestList() 4 -> clickAchievementList() 5 -> clickCombatAchievements() - 6 -> clickCollectionLog() + 6 -> clickCollectionLog(op) 7 -> selectTimePlayedToggle() else -> throw NotImplementedError("Unhandled summary click: comsub=$comsub") } @@ -72,8 +75,10 @@ constructor(private val eventBus: EventBus, private val protectedAccess: Protect } } - private fun Player.clickCollectionLog() { - ifOpenOverlay("interface.collection", eventBus) + private fun Player.clickCollectionLog(op: IfButtonOp) { + val interf = + if (op == IfButtonOp.Op2) "interface.collection_overview" else "interface.collection" + ifOpenOverlay(interf, eventBus) } private fun Player.selectTimePlayedToggle() { diff --git a/content/other/commands/src/main/kotlin/org/rsmod/content/other/commands/AdminCommands.kt b/content/other/commands/src/main/kotlin/org/rsmod/content/other/commands/AdminCommands.kt index 1e73aa031..056b457ae 100644 --- a/content/other/commands/src/main/kotlin/org/rsmod/content/other/commands/AdminCommands.kt +++ b/content/other/commands/src/main/kotlin/org/rsmod/content/other/commands/AdminCommands.kt @@ -11,6 +11,8 @@ import kotlin.math.max import kotlin.math.min import org.rsmod.annotations.InternalApi import org.rsmod.api.area.checker.AreaChecker +import org.rsmod.api.death.NpcDeathKillContext +import org.rsmod.api.death.NpcDeathKillHook import org.rsmod.api.death.prepareAdminDieTest import org.rsmod.api.instances.BossInstanceRegistry import org.rsmod.api.instances.InstanceArea @@ -24,12 +26,11 @@ import org.rsmod.api.player.cheat.adminGodMode import org.rsmod.api.player.cheat.adminMaxHit import org.rsmod.api.player.ironman.PlayerGamemode import org.rsmod.api.player.ironman.setGamemode +import org.rsmod.api.player.debug.componentClickDebug import org.rsmod.api.player.hook.TeleportType import org.rsmod.api.player.output.MiscOutput import org.rsmod.api.player.output.mes import org.rsmod.api.player.output.soundSynth -import org.rsmod.api.player.cheat.adminGodMode -import org.rsmod.api.player.cheat.adminMaxHit import org.rsmod.api.player.protect.ProtectedAccessLauncher import org.rsmod.api.player.queueDeath import org.rsmod.api.player.stat.PlayerSkillXP @@ -76,6 +77,7 @@ constructor( private val update: GameUpdate, private val areaChecker: AreaChecker, private val regions: RegionRegistry, + private val deathKillHooks: Set, private val instanceRegistry: BossInstanceRegistry, ) : PluginScript() { private val logger = InlineLogger() @@ -168,6 +170,7 @@ constructor( "Usage: ::die pvm|pvp [true|false] (second arg = in Wilderness, default false)" } onCommand("god", "Toggle god mode (invincibility)", ::god) + onCommand("componentdebug", "Toggle interface component click debug output", ::componentDebug) onCommand("maxhit", "Toggle always max hit", ::maxhit) onCommand("openbank", "Open the bank", ::bank) onCommand("transmog", "Transmog player to NPC appearance (no args to reset)", ::transmog) { @@ -176,6 +179,21 @@ constructor( onCommand("gamemode", "Set account gamemode (normal|ironman|uim|hcim)", ::gamemode) { invalidArgs = "Use as ::gamemode normal|ironman|uim|hcim" } + onCommand("interface", "Open an interface by id or RSCM name", ::openInterface) { + invalidArgs = + "Use as ::interface idOrName (ex: ::interface 219 or ::interface bankmain)" + } + onCommand("ifopen", "Open an interface by id or RSCM name", ::openInterface) { + invalidArgs = "Use as ::ifopen idOrName (ex: ::ifopen 219 or ::ifopen bankmain)" + } + onCommand( + "testloot", + "Bulk-fire an npc's death drop hooks (no combat/animation) for loot testing", + ::testLoot, + ) { + invalidArgs = + "Use as ::testloot npcName [count] (ex: ::testloot godwars_bandos_avatar 100)" + } onCommand( "instanceexit", "Teleport to an instance's exit coord", @@ -222,6 +240,14 @@ constructor( player.mes("Max hit ${if (player.adminMaxHit) "enabled" else "disabled"}.") } + private fun componentDebug(cheat: Cheat) = + with(cheat) { + player.componentClickDebug = !player.componentClickDebug + player.mes( + "Component click debug ${if (player.componentClickDebug) "enabled" else "disabled"}." + ) + } + private fun poisonTest(cheat: Cheat) = with(cheat) { val initialDamage = args.getOrNull(0)?.toIntOrNull() ?: 0 @@ -453,6 +479,31 @@ constructor( player.mes("Spawned npc `${args[1]}` (duration: $duration cycles)") } + private fun testLoot(cheat: Cheat) = + with(cheat) { + if (args.isEmpty()) { + player.mes( + "Use as ::testloot npcName [count] (ex: ::testloot godwars_bandos_avatar 100)" + ) + return + } + val typeId = "npc.${args[0]}".asRSCM() + val type = ServerCacheManager.getNpc(typeId) + if (type == null) { + player.mes("That npc does not exist: npc.${args[0]}") + return + } + val count = args.getOrNull(1)?.toIntOrNull() ?: 100 + repeat(count) { i -> + val npc = Npc(type, player.coords) + val context = NpcDeathKillContext(hero = player, npc = npc, lootTrackerEventId = i) + for (hook in deathKillHooks) { + hook.onKill(context) + } + } + player.mes("Fired death-kill hooks for `npc.${args[0]}` x$count.") + } + private fun invAdd(cheat: Cheat) = with(cheat) { val (typeName, countArg) = args.asTypeNameAndNumber(defaultNumber = 1) @@ -659,6 +710,40 @@ constructor( is InstanceArea.CopyRegions -> exitCoord } + private fun openInterface(cheat: Cheat) = + with(cheat) { + if (args.isEmpty()) { + player.mes( + "Use as ::interface idOrName (ex: ::interface 219 or ::interface bankmain)" + ) + return + } + val first = args[0] + val interfName = + if (first.toIntOrNull() != null) { + val id = first.toInt() + if (ServerCacheManager.getInterface(id) == null) { + player.mes("No interface exists with id: $id") + return + } + val resolved = RSCM.getReverseMapping(RSCMType.INTERFACE, id) + if (resolved.isEmpty()) { + player.mes("No RSCM name mapped to interface id: $id") + return + } + resolved + } else { + "interface.${args.asTypeName()}" + } + val typeId = interfName.asRSCM() + if (typeId == -1 || ServerCacheManager.getInterface(typeId) == null) { + player.mes("That interface does not exist: '$interfName'") + return + } + protectedAccess.launch(player) { ifOpenMain(interfName) } + player.mes("Opened interface: '$interfName' (id=$typeId)") + } + private fun resolveArgTypeId(arg: String, names: Map): Int? { val argAsInt = arg.toIntOrNull() if (argAsInt != null) { diff --git a/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt b/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt index 3c8f8704a..5ed4086ae 100644 --- a/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt +++ b/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt @@ -23,6 +23,7 @@ import dev.openrune.gamevals.GamevalDumper import dev.openrune.impl.GameframeTable import dev.openrune.impl.Music import dev.openrune.map.packing.MapPackers +import dev.openrune.tables.CollectionLogCategoriesTable import dev.openrune.tables.DidYouKnow import dev.openrune.tables.InstanceSettingsTable import dev.openrune.tables.PickableObjects @@ -105,6 +106,7 @@ fun tablesToPack() = listOf( SettingConfigs.settings(), DidYouKnow.didYouknow(), InstanceSettingsTable.instanceSettings(), + CollectionLogCategoriesTable.collectionLogCategories(), ShootingStars.locations(), ShopCurrencyTable.shopCurrencies(), ) diff --git a/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/SequenceServerCodec.kt b/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/SequenceServerCodec.kt index 827bd809a..70b1cc370 100644 --- a/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/SequenceServerCodec.kt +++ b/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/SequenceServerCodec.kt @@ -32,7 +32,7 @@ class SequenceServerCodec(val sequences: Map? = null) : totalDelay = seq.frameDelays!!.sum() } maxLoops = seq.maxLoops - priority = seq.priority + priority = if (seq.priority == -1) 5 else seq.priority } private fun SequenceType.getSkeletalLength(): Int = rangeEnd - rangeBegin diff --git a/or-cache/src/main/kotlin/dev/openrune/tables/CollectionLogCategoriesTable.kt b/or-cache/src/main/kotlin/dev/openrune/tables/CollectionLogCategoriesTable.kt new file mode 100644 index 000000000..6b3960b7b --- /dev/null +++ b/or-cache/src/main/kotlin/dev/openrune/tables/CollectionLogCategoriesTable.kt @@ -0,0 +1,716 @@ +package dev.openrune.tables + +import dev.openrune.definition.dbtables.dbTable +import dev.openrune.definition.util.VarType + +object CollectionLogCategoriesTable { + + const val STRUCT_ID = 0 + const val COMPLETED_VARBIT = 1 + const val COUNT_VARP_1 = 2 + const val COUNT_VARP_2 = 3 + const val COUNT_VARP_3 = 4 + const val PB_VARP_1 = 5 + const val PB_VARP_2 = 6 + + fun collectionLogCategories() = dbTable("dbtable.collection_log_categories", serverOnly = true) { + column("struct_id", STRUCT_ID, VarType.INT) + column("completed_varbit", COMPLETED_VARBIT, VarType.INT) + column("count_varp_1", COUNT_VARP_1, VarType.INT) + column("count_varp_2", COUNT_VARP_2, VarType.INT) + column("count_varp_3", COUNT_VARP_3, VarType.INT) + column("pb_varp_1", PB_VARP_1, VarType.INT) + column("pb_varp_2", PB_VARP_2, VarType.INT) + + row("dbrow.collection_log_category_abyssal_sire") { + column(STRUCT_ID, 476) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_sire_completed") + columnRSCM(COUNT_VARP_1, "varp.total_abyssalsire_kills") + } + + row("dbrow.collection_log_category_alchemical_hydra") { + column(STRUCT_ID, 539) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_hydra_boss_completed") + columnRSCM(COUNT_VARP_1, "varp.total_hydraboss_kills") + } + + row("dbrow.collection_log_category_amoxliatl") { + column(STRUCT_ID, 1016) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_amoxliatl_completed") + columnRSCM(COUNT_VARP_1, "varp.total_amoxliatl_kills") + } + + row("dbrow.collection_log_category_araxxor") { + column(STRUCT_ID, 995) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_araxxor_completed") + columnRSCM(COUNT_VARP_1, "varp.total_araxxor_kills") + } + + row("dbrow.collection_log_category_barrows_chests") { + column(STRUCT_ID, 477) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_barrows_completed") + columnRSCM(COUNT_VARP_1, "varp.total_barrows_chests") + } + + row("dbrow.collection_log_category_brutus") { + column(STRUCT_ID, 6413) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_cowboss_completed") + columnRSCM(COUNT_VARP_1, "varp.total_cowboss_kills") + } + + row("dbrow.collection_log_category_bryophyta") { + column(STRUCT_ID, 478) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_bryophyta_completed") + columnRSCM(COUNT_VARP_1, "varp.total_bryophyta_kills") + } + + row("dbrow.collection_log_category_callisto_and_artio") { + column(STRUCT_ID, 479) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_callisto_completed") + columnRSCM(COUNT_VARP_1, "varp.total_artio_kills") + columnRSCM(COUNT_VARP_2, "varp.total_callisto_kills") + } + + row("dbrow.collection_log_category_cerberus") { + column(STRUCT_ID, 480) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_cerberus_completed") + columnRSCM(COUNT_VARP_1, "varp.total_cerberus_kills") + } + + row("dbrow.collection_log_category_chaos_elemental") { + column(STRUCT_ID, 481) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_elemental_completed") + columnRSCM(COUNT_VARP_1, "varp.total_chaosele_kills") + } + + row("dbrow.collection_log_category_chaos_fanatic") { + column(STRUCT_ID, 482) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_fanatic_completed") + columnRSCM(COUNT_VARP_1, "varp.total_chaosfanatic_kills") + } + + row("dbrow.collection_log_category_commander_zilyana") { + column(STRUCT_ID, 483) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_saradomin_completed") + columnRSCM(COUNT_VARP_1, "varp.total_saradomin_kills") + } + + row("dbrow.collection_log_category_corporeal_beast") { + column(STRUCT_ID, 484) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_corp_completed") + columnRSCM(COUNT_VARP_1, "varp.total_corp_kills") + } + + row("dbrow.collection_log_category_crazy_archaeologist") { + column(STRUCT_ID, 485) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_arch_completed") + columnRSCM(COUNT_VARP_1, "varp.total_crazyarchaeologist_kills") + } + + row("dbrow.collection_log_category_dagannoth_kings") { + column(STRUCT_ID, 486) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_dagannoth_completed") + columnRSCM(COUNT_VARP_1, "varp.total_supreme_kills") + columnRSCM(COUNT_VARP_2, "varp.total_prime_kills") + columnRSCM(COUNT_VARP_3, "varp.total_rex_kills") + } + + row("dbrow.collection_log_category_deranged_archaeologist") { + column(STRUCT_ID, 4867) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_deranged_archaeologist_completed") + columnRSCM(COUNT_VARP_1, "varp.total_derangedarchaeologist_kills") + } + + row("dbrow.collection_log_category_doom_of_mokhaiotl") { + column(STRUCT_ID, 5029) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_dom_completed") + } + + row("dbrow.collection_log_category_duke_sucellus") { + column(STRUCT_ID, 4652) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_duke_completed") + columnRSCM(COUNT_VARP_1, "varp.total_duke_sucellus_kills") + } + + row("dbrow.collection_log_category_the_fight_caves") { + column(STRUCT_ID, 500) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_fight_completed") + columnRSCM(COUNT_VARP_1, "varp.total_jad_kills") + } + + row("dbrow.collection_log_category_fortis_colosseum") { + column(STRUCT_ID, 909) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_colosseum_completed") + columnRSCM(COUNT_VARP_1, "varp.total_sol_kills") + } + + row("dbrow.collection_log_category_the_gauntlet") { + column(STRUCT_ID, 605) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_gauntlet_completed") + columnRSCM(COUNT_VARP_1, "varp.total_completed_gauntlet_hm") + columnRSCM(COUNT_VARP_2, "varp.total_completed_gauntlet") + } + + row("dbrow.collection_log_category_general_graardor") { + column(STRUCT_ID, 487) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_bandos_completed") + columnRSCM(COUNT_VARP_1, "varp.total_bandos_kills") + } + + row("dbrow.collection_log_category_giant_mole") { + column(STRUCT_ID, 488) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_mole_completed") + columnRSCM(COUNT_VARP_1, "varp.total_mole_kills") + } + + row("dbrow.collection_log_category_grotesque_guardians") { + column(STRUCT_ID, 489) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_gargoyle_completed") + columnRSCM(COUNT_VARP_1, "varp.total_gargboss_kills") + } + + row("dbrow.collection_log_category_hespori") { + column(STRUCT_ID, 541) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_hespori_completed") + columnRSCM(COUNT_VARP_1, "varp.total_hespori_kills") + } + + row("dbrow.collection_log_category_the_hueycoatl") { + column(STRUCT_ID, 1015) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_hueycoatl_completed") + columnRSCM(COUNT_VARP_1, "varp.total_huey_kills") + } + + row("dbrow.collection_log_category_the_inferno") { + column(STRUCT_ID, 499) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_inferno_completed") + columnRSCM(COUNT_VARP_1, "varp.total_zuk_kills") + } + + row("dbrow.collection_log_category_kalphite_queen") { + column(STRUCT_ID, 490) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_kalphite_completed") + columnRSCM(COUNT_VARP_1, "varp.total_kalphite_kills") + } + + row("dbrow.collection_log_category_king_black_dragon") { + column(STRUCT_ID, 491) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_dragon_completed") + columnRSCM(COUNT_VARP_1, "varp.total_kbd_kills") + } + + row("dbrow.collection_log_category_kraken") { + column(STRUCT_ID, 492) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_kraken_completed") + columnRSCM(COUNT_VARP_1, "varp.total_kraken_boss_kills") + } + + row("dbrow.collection_log_category_kree_arra") { + column(STRUCT_ID, 493) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_armadyl_completed") + columnRSCM(COUNT_VARP_1, "varp.total_armadyl_kills") + } + + row("dbrow.collection_log_category_k_ril_tsutsaroth") { + column(STRUCT_ID, 494) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_zamorak_completed") + columnRSCM(COUNT_VARP_1, "varp.total_zamorak_kills") + } + + row("dbrow.collection_log_category_the_leviathan") { + column(STRUCT_ID, 4655) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_leviathan_completed") + columnRSCM(COUNT_VARP_1, "varp.total_leviathan_kills") + } + + row("dbrow.collection_log_category_the_mad_angel") { + column(STRUCT_ID, 1203) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_mad_angel_completed") + } + + row("dbrow.collection_log_category_maggot_king") { + column(STRUCT_ID, 1189) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_maggotking_completed") + columnRSCM(COUNT_VARP_1, "varp.total_maggot_king_kills") + } + + row("dbrow.collection_log_category_moons_of_peril") { + column(STRUCT_ID, 910) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_perilous_moons_completed") + } + + row("dbrow.collection_log_category_nex") { + column(STRUCT_ID, 3769) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_nex_completed") + columnRSCM(COUNT_VARP_1, "varp.total_nex_kills") + } + + row("dbrow.collection_log_category_the_nightmare") { + column(STRUCT_ID, 1263) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_nightmare_completed") + columnRSCM(COUNT_VARP_1, "varp.total_nightmare_kills") + columnRSCM(COUNT_VARP_2, "varp.total_nightmare_challenge_kills") + } + + row("dbrow.collection_log_category_obor") { + column(STRUCT_ID, 495) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_obor_completed") + columnRSCM(COUNT_VARP_1, "varp.total_hillgiant_boss_kills") + } + + row("dbrow.collection_log_category_phantom_muspah") { + column(STRUCT_ID, 4455) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_muspah_completed") + columnRSCM(COUNT_VARP_1, "varp.total_muspah_kills") + } + + row("dbrow.collection_log_category_royal_titans") { + column(STRUCT_ID, 1743) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_royal_titans_completed") + columnRSCM(COUNT_VARP_1, "varp.total_royal_titan_kills") + } + + row("dbrow.collection_log_category_sarachnis") { + column(STRUCT_ID, 601) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_sarachnis_completed") + columnRSCM(COUNT_VARP_1, "varp.total_sarachnis_kills") + } + + row("dbrow.collection_log_category_scorpia") { + column(STRUCT_ID, 496) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_scorpia_completed") + columnRSCM(COUNT_VARP_1, "varp.total_scorpia_kills") + } + + row("dbrow.collection_log_category_scurrius") { + column(STRUCT_ID, 777) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_rat_boss_completed") + columnRSCM(COUNT_VARP_1, "varp.total_rat_boss_kills") + } + + row("dbrow.collection_log_category_shellbane_gryphon") { + column(STRUCT_ID, 6337) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_gryphon_boss_completed") + columnRSCM(COUNT_VARP_1, "varp.total_gryphon_boss_kills") + } + + row("dbrow.collection_log_category_skotizo") { + column(STRUCT_ID, 497) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_skotizo_completed") + columnRSCM(COUNT_VARP_1, "varp.total_cata_boss_kills") + } + + row("dbrow.collection_log_category_tempoross") { + column(STRUCT_ID, 2867) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_tempoross_completed") + columnRSCM(COUNT_VARP_1, "varp.total_tempoross_kills") + } + + row("dbrow.collection_log_category_thermonuclear_smoke_devil") { + column(STRUCT_ID, 498) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_smoke_completed") + columnRSCM(COUNT_VARP_1, "varp.total_thermy_kills") + } + + row("dbrow.collection_log_category_vardorvis") { + column(STRUCT_ID, 4653) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_vardorvis_completed") + columnRSCM(COUNT_VARP_1, "varp.total_vardorvis_kills") + } + + row("dbrow.collection_log_category_venenatis_and_spindel") { + column(STRUCT_ID, 501) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_venenatis_completed") + columnRSCM(COUNT_VARP_1, "varp.total_spindel_kills") + columnRSCM(COUNT_VARP_2, "varp.total_venenatis_kills") + } + + row("dbrow.collection_log_category_vet_ion_and_calvar_ion") { + column(STRUCT_ID, 502) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_vetion_completed") + columnRSCM(COUNT_VARP_1, "varp.total_calvarion_kills") + columnRSCM(COUNT_VARP_2, "varp.total_vetion_kills") + } + + row("dbrow.collection_log_category_vorkath") { + column(STRUCT_ID, 503) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_vorkath_completed") + columnRSCM(COUNT_VARP_1, "varp.total_vorkath_kills") + } + + row("dbrow.collection_log_category_the_whisperer") { + column(STRUCT_ID, 4654) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_whisperer_completed") + columnRSCM(COUNT_VARP_1, "varp.total_whisperer_kills") + } + + row("dbrow.collection_log_category_wintertodt") { + column(STRUCT_ID, 504) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_wintertodt_completed") + columnRSCM(COUNT_VARP_1, "varp.total_wintertodt_kills") + } + + row("dbrow.collection_log_category_yama") { + column(STRUCT_ID, 4736) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_yama_completed") + columnRSCM(COUNT_VARP_1, "varp.total_yama_kills") + } + + row("dbrow.collection_log_category_zalcano") { + column(STRUCT_ID, 604) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_zalcano_completed") + columnRSCM(COUNT_VARP_1, "varp.total_zalcano_kills") + } + + row("dbrow.collection_log_category_zulrah") { + column(STRUCT_ID, 505) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_bosses_zulrah_completed") + columnRSCM(COUNT_VARP_1, "varp.total_snakeboss_kills") + } + + row("dbrow.collection_log_category_chambers_of_xeric") { + column(STRUCT_ID, 507) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_raids_cox_completed") + columnRSCM(COUNT_VARP_1, "varp.total_completed_xericchambers_challenge") + columnRSCM(COUNT_VARP_2, "varp.total_completed_xericchambers") + } + + row("dbrow.collection_log_category_theatre_of_blood") { + column(STRUCT_ID, 506) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_raids_tob_completed") + columnRSCM(COUNT_VARP_1, "varp.total_completed_theatreofblood") + columnRSCM(COUNT_VARP_2, "varp.total_completed_theatreofblood_story") + columnRSCM(COUNT_VARP_3, "varp.total_completed_theatreofblood_hard") + } + + row("dbrow.collection_log_category_tombs_of_amascut") { + column(STRUCT_ID, 4378) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_raids_toa_completed") + columnRSCM(COUNT_VARP_1, "varp.total_completed_tombsofamascut") + columnRSCM(COUNT_VARP_2, "varp.total_completed_tombsofamascut_entry") + columnRSCM(COUNT_VARP_3, "varp.total_completed_tombsofamascut_expert") + } + + row("dbrow.collection_log_category_beginner_treasure_trails") { + column(STRUCT_ID, 593) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_beginner_completed") + } + + row("dbrow.collection_log_category_easy_treasure_trails") { + column(STRUCT_ID, 508) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_easy_completed") + } + + row("dbrow.collection_log_category_medium_treasure_trails") { + column(STRUCT_ID, 509) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_medium_completed") + } + + row("dbrow.collection_log_category_hard_treasure_trails") { + column(STRUCT_ID, 510) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_hard_completed") + } + + row("dbrow.collection_log_category_elite_treasure_trails") { + column(STRUCT_ID, 511) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_elite_completed") + } + + row("dbrow.collection_log_category_master_treasure_trails") { + column(STRUCT_ID, 512) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_master_completed") + } + + row("dbrow.collection_log_category_hard_treasure_trails_rare") { + column(STRUCT_ID, 2869) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_hard_megarare_completed") + } + + row("dbrow.collection_log_category_elite_treasure_trails_rare") { + column(STRUCT_ID, 2870) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_elite_megarare_completed") + } + + row("dbrow.collection_log_category_master_treasure_trails_rare") { + column(STRUCT_ID, 2871) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_master_megarare_completed") + } + + row("dbrow.collection_log_category_shared_treasure_trail_rewards") { + column(STRUCT_ID, 513) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_shared_completed") + } + + row("dbrow.collection_log_category_scroll_cases") { + column(STRUCT_ID, 4874) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_clues_scroll_cases_completed") + } + + row("dbrow.collection_log_category_barbarian_assault") { + column(STRUCT_ID, 516) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_ba_completed") + } + + row("dbrow.collection_log_category_barracuda_trials") { + column(STRUCT_ID, 6338) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_barracuda_trials_completed") + } + + row("dbrow.collection_log_category_brimhaven_agility_arena") { + column(STRUCT_ID, 2873) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_brimhaven_agility_completed") + } + + row("dbrow.collection_log_category_castle_wars") { + column(STRUCT_ID, 514) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_castle_completed") + } + + row("dbrow.collection_log_category_fishing_trawler") { + column(STRUCT_ID, 520) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_trawler_completed") + } + + row("dbrow.collection_log_category_giants_foundry") { + column(STRUCT_ID, 4359) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_giantsfoundry_completed") + } + + row("dbrow.collection_log_category_gnome_restaurant") { + column(STRUCT_ID, 521) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_gnome_completed") + } + + row("dbrow.collection_log_category_guardians_of_the_rift") { + column(STRUCT_ID, 4274) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_gotr_completed") + columnRSCM(COUNT_VARP_1, "varp.total_gotr_kills") + } + + row("dbrow.collection_log_category_hallowed_sepulchre") { + column(STRUCT_ID, 1279) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_hallowed_sepulchre_completed") + } + + row("dbrow.collection_log_category_last_man_standing") { + column(STRUCT_ID, 1720) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_lms_completed") + } + + row("dbrow.collection_log_category_magic_training_arena") { + column(STRUCT_ID, 515) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_mta_completed") + } + + row("dbrow.collection_log_category_mahogany_homes") { + column(STRUCT_ID, 1689) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_constructioncontracts_completed") + } + + row("dbrow.collection_log_category_mastering_mixology") { + column(STRUCT_ID, 1017) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_mastering_mixology_completed") + } + + row("dbrow.collection_log_category_pest_control") { + column(STRUCT_ID, 518) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_pest_completed") + } + + row("dbrow.collection_log_category_rogues_den") { + column(STRUCT_ID, 522) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_rogues_completed") + } + + row("dbrow.collection_log_category_shades_of_mort_ton") { + column(STRUCT_ID, 517) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_shades_completed") + } + + row("dbrow.collection_log_category_soul_wars") { + column(STRUCT_ID, 2825) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_soul_wars_completed") + } + + row("dbrow.collection_log_category_temple_trekking") { + column(STRUCT_ID, 519) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_temple_completed") + } + + row("dbrow.collection_log_category_tithe_farm") { + column(STRUCT_ID, 524) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_tithe_completed") + } + + row("dbrow.collection_log_category_trouble_brewing") { + column(STRUCT_ID, 523) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_minigames_brewing_completed") + } + + row("dbrow.collection_log_category_vale_totems") { + column(STRUCT_ID, 5055) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_vale_totems_completed") + } + + row("dbrow.collection_log_category_volcanic_mine") { + column(STRUCT_ID, 2872) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_volcanic_mine_completed") + } + + row("dbrow.collection_log_category_aerial_fishing") { + column(STRUCT_ID, 540) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_aerial_fishing_completed") + } + + row("dbrow.collection_log_category_all_pets") { + column(STRUCT_ID, 535) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_pets_completed") + } + + row("dbrow.collection_log_category_boat_paints") { + column(STRUCT_ID, 6342) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_sailing_paint_completed") + } + + row("dbrow.collection_log_category_camdozaal") { + column(STRUCT_ID, 2885) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_camdozaal_completed") + } + + row("dbrow.collection_log_category_champion_s_challenge") { + column(STRUCT_ID, 528) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_champions_completed") + } + + row("dbrow.collection_log_category_chompy_bird_hunting") { + column(STRUCT_ID, 537) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_chompy_completed") + } + + row("dbrow.collection_log_category_colossal_wyrm_agility") { + column(STRUCT_ID, 1018) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_colossal_wyrm_agility_completed") + } + + row("dbrow.collection_log_category_creature_creation") { + column(STRUCT_ID, 546) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_creature_creation_completed") + } + + row("dbrow.collection_log_category_cyclopes") { + column(STRUCT_ID, 532) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_cyclopes_completed") + } + + row("dbrow.collection_log_category_elder_chaos_druids") { + column(STRUCT_ID, 533) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_chaos_completed") + } + + row("dbrow.collection_log_category_forestry") { + column(STRUCT_ID, 4553) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_forestry_completed") + } + + row("dbrow.collection_log_category_fossil_island_notes") { + column(STRUCT_ID, 548) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_fossil_notes_completed") + } + + row("dbrow.collection_log_category_glough_s_experiments") { + column(STRUCT_ID, 526) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_gorillas_completed") + } + + row("dbrow.collection_log_category_hunter_guild") { + column(STRUCT_ID, 911) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_hunter_guild_completed") + } + + row("dbrow.collection_log_category_lost_schematics") { + column(STRUCT_ID, 6339) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_lost_schematics_completed") + } + + row("dbrow.collection_log_category_monkey_backpacks") { + column(STRUCT_ID, 1397) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_monkey_backpack_completed") + } + + row("dbrow.collection_log_category_motherlode_mine") { + column(STRUCT_ID, 530) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_motherlode_completed") + } + + row("dbrow.collection_log_category_my_notes") { + column(STRUCT_ID, 549) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_barbarian_notes_completed") + } + + row("dbrow.collection_log_category_ocean_encounters") { + column(STRUCT_ID, 6340) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_ocean_encounters_completed") + } + + row("dbrow.collection_log_category_random_events") { + column(STRUCT_ID, 538) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_random_completed") + } + + row("dbrow.collection_log_category_revenants") { + column(STRUCT_ID, 525) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_revenants_completed") + } + + row("dbrow.collection_log_category_rooftop_agility") { + column(STRUCT_ID, 547) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_rooftop_completed") + } + + row("dbrow.collection_log_category_sailing_miscellaneous") { + column(STRUCT_ID, 6343) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_sailing_misc_completed") + } + + row("dbrow.collection_log_category_sea_treasures") { + column(STRUCT_ID, 6341) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_sea_treasures_completed") + } + + row("dbrow.collection_log_category_shayzien_armour") { + column(STRUCT_ID, 531) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_shayzien_completed") + } + + row("dbrow.collection_log_category_shooting_stars") { + column(STRUCT_ID, 2858) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_star_completed") + } + + row("dbrow.collection_log_category_skilling_pets") { + column(STRUCT_ID, 529) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_skilling_completed") + } + + row("dbrow.collection_log_category_slayer") { + column(STRUCT_ID, 527) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_slayer_completed") + } + + row("dbrow.collection_log_category_tormented_demons") { + column(STRUCT_ID, 969) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_tormented_demons_completed") + } + + row("dbrow.collection_log_category_tzhaar") { + column(STRUCT_ID, 536) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_tzhaar_completed") + } + + row("dbrow.collection_log_category_miscellaneous") { + column(STRUCT_ID, 534) + columnRSCM(COMPLETED_VARBIT, "varbit.collection_other_misc_completed") + } + + } +}