diff --git a/.gitignore b/.gitignore index 5bbbda25f..ebbb3c938 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,7 @@ CLAUDE.md logs/ *.log /.cursor -/.data/raw-cache/cs2 + +# Extracted library sources (do not commit) +/dev/ + diff --git a/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorage.kt b/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorage.kt index 74d00a198..9345b488f 100644 --- a/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorage.kt +++ b/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorage.kt @@ -28,6 +28,9 @@ constructor(private val hooks: Set<@JvmSuppressWildcards PlayerItemStorageHook>) return total } + override fun providesContentGroup(player: Player, inventory: Inventory, contentGroup: Int): Boolean = + hooks.any { it.providesContentGroup(player, contentGroup) } + public fun consumePolicy( player: Player, inventory: Inventory, diff --git a/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorageHook.kt b/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorageHook.kt index 974a54bdb..818154e74 100644 --- a/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorageHook.kt +++ b/api/inv-storage/src/main/kotlin/org/rsmod/api/inv/storage/PlayerItemStorageHook.kt @@ -1,5 +1,7 @@ package org.rsmod.api.inv.storage +import org.rsmod.game.entity.Player + /** * Virtual item storage (coal bag, rune pouch, etc.). [PlayerItemStorage] calls [shouldProcess] * before every operation — only implement the storage logic here. @@ -15,4 +17,7 @@ public interface PlayerItemStorageHook { public fun remove(ctx: PlayerItemStorageContext, amount: Int): Int public fun add(ctx: PlayerItemStorageContext, amount: Int): Int + + /** True when this storage provides any item in [contentGroup] for [player]. */ + public fun providesContentGroup(player: Player, contentGroup: Int): Boolean = false } diff --git a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt index 89af046f7..442943574 100644 --- a/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt +++ b/api/player/src/main/kotlin/org/rsmod/api/player/protect/ProtectedAccess.kt @@ -3324,15 +3324,12 @@ public class ProtectedAccess( return invTotal(inv, "obj.coins") } - public fun invContentTotal(inv: Inventory, content: String): Int { - var count = 0 - for (obj in inv) { - val filtered = obj ?: continue - if (getInvObj(filtered).isContentType(content)) { - count += filtered.count - } + public fun invContentTotal(inv: Inventory, query: String): Int { + return when { + query.startsWith("obj.") -> inv.count(query) + query.startsWith("content.") -> inv.contentTotal(query) + else -> error("Expected obj.* or content.*: $query") } - return count } public fun invTotal(inv: Inventory, obj: String): Int { @@ -3340,7 +3337,8 @@ public class ProtectedAccess( } public fun invContains(inv: Inventory, content: String): Boolean { - return inv.any { it != null && getInvObj(it).contentGroup == content.asRSCM(RSCMType.CONTENT) } + // Inventory.contains handles content groups + virtual storage (toolbelt, etc.). + return inv.contains(content) } /** @@ -3349,32 +3347,32 @@ public class ProtectedAccess( * as shops are excluded. */ public fun playerContainsContent(content: String): Boolean { - val contentId = content.asRSCM(RSCMType.CONTENT) - return playerContainsInInventories { getInvObj(it).contentGroup == contentId } + for (inventory in player.invMap.values) { + if (inventory.type.scope == InvScope.Shared) continue + if (inventory.contains(content)) { + return true + } + } + return false } /** * Returns whether any stack of [obj] exists in any of the player's inventories. Shared - * inventories such as shops are excluded. + * inventories such as shops are excluded. Includes virtual storage (toolbelt, etc.). */ public fun playerContainsObj(obj: String): Boolean { - val objId = obj.asRSCM(RSCMType.OBJ) - return playerContainsInInventories { getInvObj(it).id == objId } - } - - /** @see [playerContainsObj] */ - public fun playerContainsAnyObj(vararg objs: String): Boolean = objs.any(::playerContainsObj) - - private inline fun playerContainsInInventories(crossinline predicate: (InvObj) -> Boolean): Boolean { for (inventory in player.invMap.values) { if (inventory.type.scope == InvScope.Shared) continue - if (inventory.any { slot -> slot != null && predicate(slot) }) { + if (inventory.count(obj) > 0) { return true } } return false } + /** @see [playerContainsObj] */ + public fun playerContainsAnyObj(vararg objs: String): Boolean = objs.any(::playerContainsObj) + public fun inv(inv: String): Inventory { return player.invMap.getOrPut(inv) } diff --git a/build-logic/src/main/kotlin/base-conventions.gradle.kts b/build-logic/src/main/kotlin/base-conventions.gradle.kts index 3830bc795..ffc737e3e 100644 --- a/build-logic/src/main/kotlin/base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/base-conventions.gradle.kts @@ -5,7 +5,12 @@ plugins { id("test-conventions") } -if (path != ":or-cache" && path != ":engine:map" && path != ":engine:routefinder") { +if (path != ":or-cache" && + path != ":or-cache:pack-api" && + path != ":engine:map" && + path != ":engine:routefinder" && + !path.endsWith(":pack") +) { dependencies { implementation(project(":or-cache")) } diff --git a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/EquipmentTabScript.kt b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/EquipmentTabScript.kt index db0a6a7d8..f52858156 100644 --- a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/EquipmentTabScript.kt +++ b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/EquipmentTabScript.kt @@ -9,6 +9,7 @@ import org.rsmod.plugin.scripts.ScriptContext class EquipmentTabScript : PluginScript() { override fun ScriptContext.startup() { onIfOverlayButton("component.wornitems:call_follower") { player.callFollower() } + onIfOverlayButton("component.wornitems:tb_call_follower") { player.callFollower() } } private fun Player.callFollower() { diff --git a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/death/ItemsKeptOnDeathScript.kt b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/death/ItemsKeptOnDeathScript.kt index a7d7051f1..b0478b99c 100644 --- a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/death/ItemsKeptOnDeathScript.kt +++ b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/death/ItemsKeptOnDeathScript.kt @@ -40,6 +40,7 @@ constructor( override fun ScriptContext.startup() { onIfOverlayButton("component.wornitems:deathkeep") { player.selectKeptOnDeath() } + onIfOverlayButton("component.wornitems:tb_deathkeep") { player.selectKeptOnDeath() } onIfClose("interface.deathkeep") { player.closeKeptOnDeath() } } diff --git a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/prices/GuidePriceScript.kt b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/prices/GuidePriceScript.kt index 203419bc1..bfe776095 100644 --- a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/prices/GuidePriceScript.kt +++ b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/prices/GuidePriceScript.kt @@ -40,6 +40,7 @@ constructor( override fun ScriptContext.startup() { onIfOverlayButton("component.wornitems:pricechecker") { player.selectGuidePrices() } + onIfOverlayButton("component.wornitems:tb_pricechecker") { player.selectGuidePrices() } onIfModalButton("component.ge_pricechecker:all") { addAllFromInv() } onIfModalButton("component.ge_pricechecker_side:items") { addFromSlot(it.comsub, it.op) } onIfModalButton("component.ge_pricechecker:items") { takeFromSlot(it.comsub, it.op) } diff --git a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/stats/EquipmentStats.kt b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/stats/EquipmentStats.kt index 433eeb83d..55a55b289 100644 --- a/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/stats/EquipmentStats.kt +++ b/content/interfaces/equipment/src/main/kotlin/org/rsmod/content/interfaces/equipment/stats/EquipmentStats.kt @@ -38,6 +38,7 @@ constructor( ) : PluginScript() { override fun ScriptContext.startup() { onIfOverlayButton("component.wornitems:equipment") { player.selectStats() } + onIfOverlayButton("component.wornitems:tb_equipment") { player.selectStats() } val componentWornSlots = equipment_stats_to_slots_map.filterValuesNotNull().map { it.key to RSCM.getReverseMapping(RSCMType.COMPONENT,it.value.packed) } for ((slot, component) in componentWornSlots) { diff --git a/content/other/toolbelt/build.gradle.kts b/content/other/toolbelt/build.gradle.kts new file mode 100644 index 000000000..14e5fba27 --- /dev/null +++ b/content/other/toolbelt/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("base-conventions") +} + +dependencies { + implementation(projects.api.pluginCommons) + implementation(projects.api.attr) + implementation(projects.api.invStorage) + implementation(projects.content.other.toolbelt.pack) + implementation(projects.content.skills.utils) + implementation(libs.jackson.databind) + implementation(libs.jackson.dataformat.toml) + implementation(libs.jackson.module.kotlin) +} diff --git a/content/other/toolbelt/pack/build.gradle.kts b/content/other/toolbelt/pack/build.gradle.kts new file mode 100644 index 000000000..370971205 --- /dev/null +++ b/content/other/toolbelt/pack/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("base-conventions") +} + +dependencies { + implementation(projects.orCache.packApi) + implementation(libs.or2.all.cache) + implementation(libs.or2.tools) + implementation(libs.or2.definition) + implementation(libs.or2.filestore) + implementation(libs.or2.filesystem) + implementation(libs.jackson.databind) + implementation(libs.jackson.dataformat.toml) + implementation("io.netty:netty-buffer:4.1.107.Final") + implementation("me.tongfei:progressbar:0.9.2") + implementation("com.michael-bull.kotlin-inline-logger:kotlin-inline-logger:1.0.6") +} diff --git a/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltInterface.kt b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltInterface.kt new file mode 100644 index 000000000..4262fa74a --- /dev/null +++ b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltInterface.kt @@ -0,0 +1,52 @@ +package org.rsmod.content.other.toolbelt.pack + +import dev.openrune.cache.tools.iftype.dsl.buildInterface +import dev.openrune.cache.tools.iftype.dsl.impl.layer +import dev.openrune.definition.constants.ConstantProvider +import dev.openrune.definition.type.widget.IfEvent + +private const val WIDTH = 419 +private const val HEIGHT = 291 +private const val INSET = 10 +private const val TITLE_H = 36 +private const val SEARCH_H = 24 +private const val SEARCH_GAP = 4 +private const val SCROLL_W = 16 +private const val CONTENT_TOP = TITLE_H + SEARCH_H + SEARCH_GAP + +fun buildToolbeltInterface() = + buildInterface(internalName = "interface.toolbelt", width = WIDTH, height = HEIGHT) { + val iface = ConstantProvider.getMapping("interface.toolbelt") + val initCs = ConstantProvider.getMapping("clientscript.toolbelt_init") + fun comp(child: Int) = (iface shl 16) or child + + onLoadListener { arrayOf(initCs, comp(1), comp(2), comp(3), comp(4)) } + + layer("border") { + position { 0 to 0 } + size { WIDTH to HEIGHT } + noClickThrough { true } + } + + layer("search") { + position { INSET to TITLE_H } + size { WIDTH - INSET * 2 to SEARCH_H } + } + + layer("content") { + position { INSET to CONTENT_TOP } + size { WIDTH - INSET * 2 - SCROLL_W - 2 to HEIGHT - CONTENT_TOP - INSET } + scrollHeight { 0 } + events = (IfEvent.Op1.bitmask or IfEvent.Op10.bitmask).toInt() + } + + layer("scrollbar") { + position { WIDTH - INSET - SCROLL_W to CONTENT_TOP } + size { SCROLL_W to HEIGHT - CONTENT_TOP - INSET } + } + + layer("highlight") { + position { 0 to 0 } + size { WIDTH to HEIGHT } + } + } diff --git a/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltPackSettings.kt b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltPackSettings.kt new file mode 100644 index 000000000..33bd4471b --- /dev/null +++ b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltPackSettings.kt @@ -0,0 +1,34 @@ +package org.rsmod.content.other.toolbelt.pack + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.toml.TomlFactory +import java.io.File + +object ToolbeltPackSettings { + private val mapper = ObjectMapper(TomlFactory()) + + private const val PLUGIN = "content/other/toolbelt/src/main/resources" + + fun isEnabled(projectRoot: File = File("..")): Boolean { + val file = File(projectRoot, "$PLUGIN/toolbelt.toml") + if (!file.isFile) { + return false + } + return runCatching { + val root = mapper.readTree(file) + root.path("is_enabled").asBoolean(true) + }.getOrDefault(false) + } + + fun configDirectory(projectRoot: File = File("..")): File = + File(projectRoot, "$PLUGIN/pack/configs") + + fun spriteDirectory(projectRoot: File = File("..")): File = + File(projectRoot, "$PLUGIN/pack/sprites") + + fun scriptDirectory(projectRoot: File = File("..")): File = + File(projectRoot, "$PLUGIN/pack/scripts") + + fun cs2Directory(projectRoot: File = File("..")): File = + File(projectRoot, "$PLUGIN/pack/cs2") +} diff --git a/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltPluginPack.kt b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltPluginPack.kt new file mode 100644 index 000000000..c329c9bc6 --- /dev/null +++ b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltPluginPack.kt @@ -0,0 +1,33 @@ +package org.rsmod.content.other.toolbelt.pack + +import dev.openrune.cache.filestore.definition.InterfaceType +import dev.openrune.definition.dbtables.DBTable +import dev.openrune.pack.PluginPack +import java.io.File + +class ToolbeltPluginPack : PluginPack() { + override fun shouldAlwaysPack(): Boolean = true + + override fun isEnabled(projectRoot: File): Boolean = ToolbeltPackSettings.isEnabled(projectRoot) + + override fun packScriptDirectories(projectRoot: File): List { + if (!isEnabled(projectRoot)) { + return emptyList() + } + return listOf(ToolbeltPackSettings.scriptDirectory(projectRoot)) + } + + override fun configDirectories(projectRoot: File): List = + listOf(ToolbeltPackSettings.configDirectory(projectRoot)) + + override fun spriteDirectories(projectRoot: File): List = + listOf(ToolbeltPackSettings.spriteDirectory(projectRoot)) + + override fun cs2Directories(projectRoot: File): List = + listOf(ToolbeltPackSettings.cs2Directory(projectRoot)) + + override fun dbTables(): List = listOf(ToolbeltTable.slots()) + + override fun interfaces(): List = + listOf(buildWornItemsToolbelt(), buildToolbeltInterface()) +} diff --git a/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltTable.kt b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltTable.kt new file mode 100644 index 000000000..8efbbf99b --- /dev/null +++ b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/ToolbeltTable.kt @@ -0,0 +1,390 @@ +package org.rsmod.content.other.toolbelt.pack + +import dev.openrune.definition.dbtables.dbTable +import dev.openrune.definition.util.VarType + +object ToolbeltTable { + const val COL_SLOT_INDEX = 0 + const val COL_DISPLAY_NAME = 1 + const val COL_ALLOWED_ITEMS = 2 + const val COL_REMOVABLE = 3 + const val COL_TIERED = 4 + const val COL_SKILL_STAT = 5 + const val COL_REQUIRE_LEVEL = 6 + const val COL_CATEGORY = 7 + const val COL_UNLOCK_HINT = 8 + const val COL_HAS_SETTINGS = 9 + const val COL_UNLOCK_BIT = 10 + const val COL_STORE_VARBIT = 11 + + fun slots() = + dbTable("dbtable.toolbelt_slots", serverOnly = false) { + column("slot_index", COL_SLOT_INDEX, VarType.INT) + column("display_name", COL_DISPLAY_NAME, VarType.STRING) + column("allowed_items", COL_ALLOWED_ITEMS, VarType.OBJ) + column("removable", COL_REMOVABLE, VarType.BOOLEAN) + column("tiered", COL_TIERED, VarType.BOOLEAN) + column("skill_stat", COL_SKILL_STAT, VarType.STAT) + column("require_level", COL_REQUIRE_LEVEL, VarType.INT) + column("category", COL_CATEGORY, VarType.INT) + column("unlock_hint", COL_UNLOCK_HINT, VarType.STRING) + column("has_settings", COL_HAS_SETTINGS, VarType.BOOLEAN) + column("unlock_bit", COL_UNLOCK_BIT, VarType.INT) + column("store_varbit", COL_STORE_VARBIT, VarType.VARP) + + row("dbrow.toolbelt_pickaxe") { + column(COL_SLOT_INDEX, 0) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_0") + column(COL_DISPLAY_NAME, "Pickaxe") + columnRSCM( + COL_ALLOWED_ITEMS, + "obj.bronze_pickaxe", + "obj.iron_pickaxe", + "obj.steel_pickaxe", + "obj.black_pickaxe", + "obj.mithril_pickaxe", + "obj.adamant_pickaxe", + "obj.rune_pickaxe", + "obj.dragon_pickaxe", + "obj.dragon_pickaxe_pretty", + "obj.infernal_pickaxe", + "obj.infernal_pickaxe_empty", + "obj.crystal_pickaxe", + "obj.crystal_pickaxe_inactive", + "obj.3a_pickaxe", + "obj.trailblazer_pickaxe", + "obj.trailblazer_pickaxe_empty", + "obj.trailblazer_pickaxe_no_infernal", + "obj.trailblazer_reloaded_pickaxe", + "obj.trailblazer_reloaded_pickaxe_empty", + "obj.trailblazer_reloaded_pickaxe_no_infernal", + ) + column(COL_REMOVABLE, true) + column(COL_TIERED, true) + columnRSCM(COL_SKILL_STAT, "stat.mining") + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_axe") { + column(COL_SLOT_INDEX, 1) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_1") + column(COL_DISPLAY_NAME, "Axe") + columnRSCM( + COL_ALLOWED_ITEMS, + "obj.bronze_axe", + "obj.iron_axe", + "obj.steel_axe", + "obj.black_axe", + "obj.mithril_axe", + "obj.adamant_axe", + "obj.rune_axe", + "obj.dragon_axe", + "obj.infernal_axe", + "obj.infernal_axe_empty", + "obj.crystal_axe", + "obj.crystal_axe_inactive", + "obj.3a_axe", + "obj.trailblazer_axe", + "obj.trailblazer_axe_empty", + "obj.trailblazer_axe_no_infernal", + "obj.trailblazer_reloaded_axe", + "obj.trailblazer_reloaded_axe_empty", + "obj.trailblazer_reloaded_axe_no_infernal", + ) + column(COL_REMOVABLE, true) + column(COL_TIERED, true) + columnRSCM(COL_SKILL_STAT, "stat.woodcutting") + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_hammer") { + column(COL_SLOT_INDEX, 2) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_2") + column(COL_DISPLAY_NAME, "Hammer") + columnRSCM( + COL_ALLOWED_ITEMS, + "obj.hammer", + "obj.imcando_hammer", + "obj.imcando_hammer_offhand", + ) + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_chisel") { + column(COL_SLOT_INDEX, 3) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_3") + column(COL_DISPLAY_NAME, "Chisel") + columnRSCM(COL_ALLOWED_ITEMS, "obj.chisel") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_knife") { + column(COL_SLOT_INDEX, 4) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_4") + column(COL_DISPLAY_NAME, "Knife") + columnRSCM(COL_ALLOWED_ITEMS, "obj.knife") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_tinderbox") { + column(COL_SLOT_INDEX, 5) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_5") + column(COL_DISPLAY_NAME, "Tinderbox") + columnRSCM(COL_ALLOWED_ITEMS, "obj.tinderbox") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_pestle_and_mortar") { + column(COL_SLOT_INDEX, 6) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_6") + column(COL_DISPLAY_NAME, "Pestle and mortar") + columnRSCM(COL_ALLOWED_ITEMS, "obj.pestle_and_mortar") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_spade") { + column(COL_SLOT_INDEX, 7) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_7") + column(COL_DISPLAY_NAME, "Spade") + columnRSCM(COL_ALLOWED_ITEMS, "obj.spade") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 1) + } + + row("dbrow.toolbelt_rake") { + column(COL_SLOT_INDEX, 8) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_8") + column(COL_DISPLAY_NAME, "Rake") + columnRSCM(COL_ALLOWED_ITEMS, "obj.rake") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 1) + } + + row("dbrow.toolbelt_dibber") { + column(COL_SLOT_INDEX, 9) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_9") + column(COL_DISPLAY_NAME, "Seed dibber") + columnRSCM(COL_ALLOWED_ITEMS, "obj.dibber") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 1) + } + + row("dbrow.toolbelt_secateurs") { + column(COL_SLOT_INDEX, 10) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_10") + column(COL_DISPLAY_NAME, "Secateurs") + columnRSCM(COL_ALLOWED_ITEMS, "obj.secateurs", "obj.fairy_queen_secateurs") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 1) + } + + row("dbrow.toolbelt_gardening_trowel") { + column(COL_SLOT_INDEX, 11) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_11") + column(COL_DISPLAY_NAME, "Gardening trowel") + columnRSCM(COL_ALLOWED_ITEMS, "obj.gardening_trowel") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 1) + } + + row("dbrow.toolbelt_watering_can") { + column(COL_SLOT_INDEX, 12) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_12") + column(COL_DISPLAY_NAME, "Watering can") + columnRSCM( + COL_ALLOWED_ITEMS, + "obj.watering_can_0", + "obj.watering_can_1", + "obj.watering_can_2", + "obj.watering_can_3", + "obj.watering_can_4", + "obj.watering_can_5", + "obj.watering_can_6", + "obj.watering_can_7", + "obj.watering_can_8", + "obj.zeah_wateringcan", + ) + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 1) + } + + row("dbrow.toolbelt_shears") { + column(COL_SLOT_INDEX, 13) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_13") + column(COL_DISPLAY_NAME, "Shears") + columnRSCM(COL_ALLOWED_ITEMS, "obj.shears") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 0) + } + + row("dbrow.toolbelt_needle") { + column(COL_SLOT_INDEX, 14) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_14") + column(COL_DISPLAY_NAME, "Needle") + columnRSCM(COL_ALLOWED_ITEMS, "obj.needle") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 3) + } + + row("dbrow.toolbelt_glassblowing_pipe") { + column(COL_SLOT_INDEX, 15) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_15") + column(COL_DISPLAY_NAME, "Glassblowing pipe") + columnRSCM(COL_ALLOWED_ITEMS, "obj.glassblowingpipe") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 3) + } + + row("dbrow.toolbelt_small_net") { + column(COL_SLOT_INDEX, 16) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_16") + column(COL_DISPLAY_NAME, "Small fishing net") + columnRSCM(COL_ALLOWED_ITEMS, "obj.net") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 2) + } + + row("dbrow.toolbelt_big_net") { + column(COL_SLOT_INDEX, 17) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_17") + column(COL_DISPLAY_NAME, "Big fishing net") + columnRSCM(COL_ALLOWED_ITEMS, "obj.big_net") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 2) + } + + row("dbrow.toolbelt_fishing_rod") { + column(COL_SLOT_INDEX, 18) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_18") + column(COL_DISPLAY_NAME, "Fishing rod") + columnRSCM(COL_ALLOWED_ITEMS, "obj.fishing_rod") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 2) + } + + row("dbrow.toolbelt_fly_fishing_rod") { + column(COL_SLOT_INDEX, 19) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_19") + column(COL_DISPLAY_NAME, "Fly fishing rod") + columnRSCM(COL_ALLOWED_ITEMS, "obj.fly_fishing_rod") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 2) + } + + row("dbrow.toolbelt_harpoon") { + column(COL_SLOT_INDEX, 20) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_20") + column(COL_DISPLAY_NAME, "Harpoon") + columnRSCM( + COL_ALLOWED_ITEMS, + "obj.harpoon", + "obj.infernal_harpoon", + "obj.infernal_harpoon_empty", + ) + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 2) + } + + row("dbrow.toolbelt_lobster_pot") { + column(COL_SLOT_INDEX, 21) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_21") + column(COL_DISPLAY_NAME, "Lobster pot") + columnRSCM(COL_ALLOWED_ITEMS, "obj.lobster_pot") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 2) + } + + row("dbrow.toolbelt_amulet_mould") { + column(COL_SLOT_INDEX, 22) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_22") + column(COL_DISPLAY_NAME, "Amulet mould") + columnRSCM(COL_ALLOWED_ITEMS, "obj.amulet_mould") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 3) + } + + row("dbrow.toolbelt_necklace_mould") { + column(COL_SLOT_INDEX, 23) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_23") + column(COL_DISPLAY_NAME, "Necklace mould") + columnRSCM(COL_ALLOWED_ITEMS, "obj.necklace_mould") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 3) + } + + row("dbrow.toolbelt_ring_mould") { + column(COL_SLOT_INDEX, 24) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_24") + column(COL_DISPLAY_NAME, "Ring mould") + columnRSCM(COL_ALLOWED_ITEMS, "obj.ring_mould") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 3) + } + + row("dbrow.toolbelt_bracelet_mould") { + column(COL_SLOT_INDEX, 25) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_25") + column(COL_DISPLAY_NAME, "Bracelet mould") + columnRSCM(COL_ALLOWED_ITEMS, "obj.jewl_bracelet_mould") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 3) + } + + row("dbrow.toolbelt_tiara_mould") { + column(COL_SLOT_INDEX, 26) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_26") + column(COL_DISPLAY_NAME, "Tiara mould") + columnRSCM(COL_ALLOWED_ITEMS, "obj.tiara_mould") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 3) + } + + row("dbrow.toolbelt_bonecrusher") { + column(COL_SLOT_INDEX, 27) + columnRSCM(COL_STORE_VARBIT, "varp.toolbelt_slot_27") + column(COL_DISPLAY_NAME, "Bonecrusher") + columnRSCM(COL_ALLOWED_ITEMS, "obj.bonecrusher") + column(COL_REMOVABLE, true) + column(COL_TIERED, false) + column(COL_CATEGORY, 4) + columnRSCM(COL_SKILL_STAT, "stat.prayer") + column(COL_REQUIRE_LEVEL, 40) + column( + COL_UNLOCK_HINT, + "Speak to the Ghost Disciple at the Ectofuntus while carrying a bonecrusher and pay 2,000 Ectotokens to unlock adding it to your tool belt.", + ) + column(COL_HAS_SETTINGS, true) + column(COL_UNLOCK_BIT, 0) + } + } +} diff --git a/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/WornItems.kt b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/WornItems.kt new file mode 100644 index 000000000..0f61bf19b --- /dev/null +++ b/content/other/toolbelt/pack/src/main/kotlin/org/rsmod/content/other/toolbelt/pack/WornItems.kt @@ -0,0 +1,106 @@ +package org.rsmod.content.other.toolbelt.pack + +import dev.openrune.cache.tools.iftype.dsl.InterfaceBuilder +import dev.openrune.cache.tools.iftype.dsl.buildInterface +import dev.openrune.cache.tools.iftype.dsl.impl.graphic +import dev.openrune.cache.tools.iftype.dsl.impl.layer +import dev.openrune.definition.constants.ConstantProvider + +private const val BTN = 34 +private const val ICON = 26 +private const val BTN_Y = 212 +private const val START_X = 4 +private const val STEP = 37 + +const val WORN_WIDTH = 190 + +const val WORN_HEIGHT = 261 + +val WORN_FOOTER_VANILLA = listOf( + "component.wornitems:equipment", + "component.wornitems:equipment_icon", + "component.wornitems:pricechecker", + "component.wornitems:com_4", + "component.wornitems:deathkeep", + "component.wornitems:com_6", + "component.wornitems:call_follower", + "component.wornitems:com_8" +) + +val WORN_FOOTER_TOOLBELT_ROW = listOf( + "component.wornitems:tb_equipment", + "component.wornitems:tb_equipment_icon", + "component.wornitems:tb_pricechecker", + "component.wornitems:tb_pricechecker_icon", + "component.wornitems:tb_deathkeep", + "component.wornitems:tb_deathkeep_icon", + "component.wornitems:tb_call_follower", + "component.wornitems:tb_call_follower_icon", + "component.wornitems:tb_toolbelt", + "component.wornitems:tb_toolbelt_icon" +) + +fun buildWornItemsToolbelt() = + buildInterface(internalName = "interface.wornitems", width = WORN_WIDTH, height = WORN_HEIGHT) { + inherit("interface.wornitems") + + tbFooterButton("tb_equipment", "equipment", 0) + tbFooterIcon("tb_equipment_icon", "equipment_icon", 0) + + tbFooterButton("tb_pricechecker", "pricechecker", 1) + tbFooterIcon("tb_pricechecker_icon", "com_4", 1) + + tbFooterButton("tb_deathkeep", "deathkeep", 2) + tbFooterIcon("tb_deathkeep_icon", "com_6", 2) + + tbFooterButton("tb_call_follower", "call_follower", 3) + tbFooterIcon("tb_call_follower_icon", "com_8", 3) + + layer("tb_toolbelt") { + from("equipment") + hide { true } + position { btnPos(4) } + size { BTN to BTN } + addOption("View toolbelt") + } + graphic("tb_toolbelt_icon") { + from("com_8") + hide { true } + position { iconPos(4) } + size { ICON to ICON } + spriteId { ConstantProvider.getMapping("sprites.tool_belt") } + } + } + +private fun InterfaceBuilder.tbFooterButton( + name: String, + donor: String, + index: Int, +) { + layer(name) { + from(donor) + hide { true } + position { btnPos(index) } + size { BTN to BTN } + } +} + +private fun InterfaceBuilder.tbFooterIcon( + name: String, + donor: String, + index: Int, +) { + graphic(name) { + from(donor) + hide { true } + position { iconPos(index) } + size { ICON to ICON } + } +} + +fun btnPos(index: Int): Pair = (START_X + index * STEP) to BTN_Y + +fun iconPos(index: Int): Pair { + val pad = (BTN - ICON) / 2 + return (START_X + index * STEP + pad) to (BTN_Y + pad) +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/Toolbelt.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/Toolbelt.kt new file mode 100644 index 000000000..a8cf58f50 --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/Toolbelt.kt @@ -0,0 +1,236 @@ +package org.rsmod.content.other.toolbelt + +import dev.openrune.ServerCacheManager +import dev.openrune.rscm.RSCM.asRSCM +import dev.openrune.rscm.RSCMType +import dev.openrune.types.ItemServerType +import org.rsmod.api.config.refs.params +import org.rsmod.api.player.stat.statBase +import org.rsmod.api.table.ToolbeltSlotsRow +import org.rsmod.game.entity.Player +import org.rsmod.game.inv.InvObj + +object Toolbelt { + const val ADD_OPTION = "Add-to-toolbelt" + const val OWNED_COUNT = 1 + + private val settings: ToolbeltSettings + get() = ToolbeltSettings.load() + + fun isEnabled(): Boolean = settings.isEnabled + + fun allowHigherTierUnlock(): Boolean = settings.allowHigherTierUnlock + + fun validateStorageOnLogin(player: Player) { + if (!isEnabled()) { + return + } + for (slot in rows()) { + val index = storedItemIndex(player, slot) ?: continue + val type = slot.allowedItems.getOrNull(index) ?: run { + ToolbeltVars.clearSlot(player, slot.slotIndex) + continue + } + if (!slot.accepts(type)) { + ToolbeltVars.clearSlot(player, slot.slotIndex) + } + } + } + + fun slotFor(type: ItemServerType): ToolbeltSlotsRow? { + if (type.isCert || type.isPlaceholder || type.isDummyItem) { + return null + } + return ToolbeltSlotsRow.all().firstOrNull { it.accepts(type) } + } + + fun slotFor(internalName: String): ToolbeltSlotsRow? { + val name = + if (internalName.startsWith("obj.")) { + internalName + } else { + "obj.$internalName" + } + return ToolbeltSlotsRow.all().firstOrNull { it.accepts(name) } + } + + fun has(player: Player, slot: ToolbeltSlotsRow): Boolean = storedType(player, slot) != null + + fun storedType(player: Player, slot: ToolbeltSlotsRow): ItemServerType? { + val index = storedItemIndex(player, slot) ?: return null + return slot.allowedItems.getOrNull(index) + } + + fun displayType(player: Player, slot: ToolbeltSlotsRow): ItemServerType { + return storedType(player, slot) ?: slot.defaultItem + } + + fun provides(player: Player, itemInternal: String): Boolean { + if (!isEnabled()) { + return false + } + val type = ServerCacheManager.getItem(itemInternal.asObjId()) ?: return false + val slot = slotFor(type) ?: return false + if (!ToolbeltUnlocks.isUnlocked(player, slot)) { + return false + } + val stored = storedType(player, slot) ?: return false + if (stored.id == type.id) { + return true + } + if (!allowHigherTierUnlock() || !slot.tiered) { + return false + } + return slot.accepts(type) && + toolTier(type) <= toolTier(stored) && + meetsLevelReq(player, slot, type) + } + + fun providesContentGroup(player: Player, contentGroup: Int): Boolean { + if (!isEnabled() || contentGroup < 0) { + return false + } + return occupiedSlots(player).any { (_, type) -> type.contentGroup == contentGroup } + } + + fun resolveAxe(player: Player): InvObj? = + ToolbeltSlotsRow.getRow("dbrow.toolbelt_axe").let { resolveTieredTool(player, it) } + + fun resolvePickaxe(player: Player): InvObj? = + ToolbeltSlotsRow.getRow("dbrow.toolbelt_pickaxe").let { resolveTieredTool(player, it) } + + fun resolveExact(player: Player, slot: ToolbeltSlotsRow): InvObj? { + if (!isEnabled()) { + return null + } + val stored = storedType(player, slot) ?: return null + return InvObj(stored, OWNED_COUNT) + } + + fun occupiedSlots(player: Player): List> = + rows().mapNotNull { slot -> + storedType(player, slot)?.let { slot to it } + } + + sealed class AddResult { + data class Added(val item: ItemServerType) : AddResult() + + data class Replaced(val added: ItemServerType, val returned: ItemServerType) : AddResult() + + data class Failed(val message: String) : AddResult() + } + + fun tryAdd(player: Player, type: ItemServerType): AddResult = + addInternal(player, type, bypassRequirements = false) + + fun grantOwned(player: Player, type: ItemServerType, bypassRequirements: Boolean = false): AddResult = + addInternal(player, type, bypassRequirements = bypassRequirements) + + fun tryRemove(player: Player, slot: ToolbeltSlotsRow): ItemServerType? { + if (!isEnabled() || !slot.removable) { + return null + } + val existing = storedType(player, slot) ?: return null + ToolbeltVars.clearSlot(player, slot.slotIndex) + return existing + } + + fun allEligibleItems(): List = rows().flatMap { it.allowedItems } + + private fun addInternal( + player: Player, + type: ItemServerType, + bypassRequirements: Boolean, + ): AddResult { + if (!isEnabled()) { + return AddResult.Failed("The tool belt is currently disabled.") + } + val slot = + slotFor(type) ?: return AddResult.Failed("You can't add that to your tool belt.") + if (!bypassRequirements) { + if (!ToolbeltUnlocks.isUnlocked(player, slot)) { + return AddResult.Failed(ToolbeltUnlocks.unlockMessage(slot)) + } + if (!meetsSlotAddRequirement(player, slot)) { + val req = slot.requireLevel ?: 1 + val skill = slot.skillStat?.displayName?.ifBlank { null } ?: "skill" + return AddResult.Failed("You need a $skill level of $req to add that to your tool belt.") + } + } + + val existing = storedType(player, slot) + if (existing != null && existing.id == type.id) { + return AddResult.Failed("Your tool belt already contains that.") + } + + setStored(player, slot, type) + return if (existing != null) { + AddResult.Replaced(type, existing) + } else { + AddResult.Added(type) + } + } + + private fun setStored(player: Player, slot: ToolbeltSlotsRow, type: ItemServerType) { + val index = slot.allowedItems.indexOfFirst { it.id == type.id } + require(index >= 0) { "Item ${type.internalName} not in ${slot.displayName} slot list" } + ToolbeltVars.setStoredAllowedIndex(player, slot.slotIndex, index + 1) + } + + private fun storedItemIndex(player: Player, slot: ToolbeltSlotsRow): Int? { + val stored = ToolbeltVars.readStoredAllowedIndex(player, slot.slotIndex) + if (stored <= 0) { + return null + } + val index = stored - 1 + if (index !in slot.allowedItems.indices) { + return null + } + return index + } + + private fun rows(): List = + ToolbeltSlotsRow.all().sortedBy { it.slotIndex } + + private fun resolveTieredTool(player: Player, slot: ToolbeltSlotsRow): InvObj? { + if (!isEnabled()) { + return null + } + val stored = storedType(player, slot) ?: return null + if (!allowHigherTierUnlock() || !slot.tiered) { + return if (meetsLevelReq(player, slot, stored)) InvObj(stored, OWNED_COUNT) else null + } + + val storedTier = toolTier(stored) + val candidates = + slot.allowedItems + .filter { toolTier(it) <= storedTier } + .filter { meetsLevelReq(player, slot, it) } + + val best = + candidates.maxWithOrNull(compareBy { toolTier(it) }.thenBy { it.id }) + ?: return null + return InvObj(best, OWNED_COUNT) + } + + private fun toolTier(type: ItemServerType): Int = levelReq(type) + + private fun levelReq(type: ItemServerType): Int = type.paramOrNull(params.levelrequire) ?: 1 + + private fun meetsSlotAddRequirement(player: Player, slot: ToolbeltSlotsRow): Boolean { + val level = slot.requireLevel ?: return true + val skill = slot.skillStat ?: return true + return player.statBase(skill.internalName) >= level + } + + private fun meetsLevelReq( + player: Player, + slot: ToolbeltSlotsRow, + type: ItemServerType, + ): Boolean { + val skill = slot.skillStat ?: return true + return player.statBase(skill.internalName) >= levelReq(type) + } +} + +private fun String.asObjId(): Int = asRSCM(RSCMType.OBJ) diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltBonecrusher.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltBonecrusher.kt new file mode 100644 index 000000000..c1d635cb6 --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltBonecrusher.kt @@ -0,0 +1,144 @@ +package org.rsmod.content.other.toolbelt + +import org.rsmod.api.config.constants +import org.rsmod.api.invtx.invAdd +import org.rsmod.api.invtx.invDel +import org.rsmod.api.player.output.mes +import org.rsmod.api.player.protect.ProtectedAccess +import org.rsmod.api.player.vars.boolVarBit +import org.rsmod.api.player.vars.intVarBit +import org.rsmod.api.table.ToolbeltSlotsRow +import org.rsmod.game.entity.Player +import org.rsmod.game.inv.isType + +object ToolbeltBonecrusher { + const val ITEM = "obj.bonecrusher" + private const val ECTO_TOKEN = "obj.ectotoken" + private const val CHARGES_PER_TOKEN = 25 + private const val MAX_CHARGES = 60_000 + + private fun bonecrusherSlot(): ToolbeltSlotsRow? = Toolbelt.slotFor(ITEM) + + fun hasOnToolbelt(player: Player): Boolean { + val slot = bonecrusherSlot() ?: return false + return Toolbelt.has(player, slot) + } + + fun checkCharges(access: ProtectedAccess): Boolean { + if (!hasOnToolbelt(access.player)) { + return false + } + val charges = access.player.bonecrusherCharges + when { + charges == 0 -> + access.mes("The bonecrusher has no charges. It can be charged with ectotokens.") + isActivityEnabled(access.player) -> + access.mes( + "The bonecrusher has $charges charges. It is active and ready to crush bones.", + ) + else -> + access.mes( + "The bonecrusher has $charges charges. It has been deactivated, and will not crush bones now.", + ) + } + return true + } + + fun charge(access: ProtectedAccess): Boolean { + if (!hasOnToolbelt(access.player)) { + return false + } + val tokenSlot = access.inv.indexOfFirst { it?.isType(ECTO_TOKEN) == true } + if (tokenSlot < 0) { + access.mes("You need ectotokens to charge the bonecrusher.") + return true + } + val tokenCount = access.inv.count(ECTO_TOKEN) + val removed = access.invDel(access.inv, ECTO_TOKEN, count = tokenCount, slot = tokenSlot) + if (removed.failure) { + return true + } + val add = tokenCount * CHARGES_PER_TOKEN + val next = (access.player.bonecrusherCharges + add).coerceAtMost(MAX_CHARGES) + if (next == access.player.bonecrusherCharges) { + access.invAdd(access.inv, ECTO_TOKEN, count = tokenCount, strict = false) + access.mes("The bonecrusher cannot hold any more charges.") + return true + } + access.player.bonecrusherCharges = next + access.mes(chargeStatusMessage(access.player, access.player.bonecrusherCharges)) + return true + } + + fun toggleActivity(access: ProtectedAccess): Boolean { + if (!hasOnToolbelt(access.player)) { + return false + } + val enabled = toggleActivity(access.player) + access.mes(activityToggleMessage(enabled)) + return true + } + + suspend fun uncharge(access: ProtectedAccess): Boolean { + if (!hasOnToolbelt(access.player)) { + return false + } + val stored = access.player.bonecrusherCharges + when { + stored == 0 -> access.mes("The bonecrusher has no charges.") + stored < CHARGES_PER_TOKEN -> + access.mes("The bonecrusher does not have enough charges for you to remove any ectotokens.") + else -> { + val redeemableTokens = stored / CHARGES_PER_TOKEN + val chargesToRemove = redeemableTokens * CHARGES_PER_TOKEN + val added = access.invAdd(access.inv, ECTO_TOKEN, count = redeemableTokens) + if (added.failure) { + access.mes(constants.dm_invspace) + return true + } + val remaining = stored - chargesToRemove + access.player.bonecrusherCharges = remaining.coerceAtLeast(0) + if (remaining == 0) { + access.doubleobjbox( + ITEM, + ECTO_TOKEN, + "You remove all the charges from the bonecrusher.", + ) + } else { + access.doubleobjbox( + ITEM, + ECTO_TOKEN, + "The bonecrusher has $remaining charges left.", + ) + } + } + } + return true + } + + fun toggleActivity(player: Player): Boolean { + val next = !player.bonecrusherActivityEnabled + player.bonecrusherActivityEnabled = next + return next + } + + fun isActivityEnabled(player: Player): Boolean = player.bonecrusherActivityEnabled + + fun activityToggleMessage(enabled: Boolean): String = + if (enabled) { + "The bonecrusher is active and ready to crush bones." + } else { + "The bonecrusher has been deactivated, and will not crush bones now." + } + + private fun chargeStatusMessage(player: Player, charges: Int): String = + if (isActivityEnabled(player)) { + "The bonecrusher has $charges charges. It is active and ready to crush bones." + } else { + "The bonecrusher has $charges charges. It has been deactivated, and will not crush bones now." + } + + private var Player.bonecrusherActivityEnabled by boolVarBit("varbit.bonecrusher_activity_enabled") + + private var Player.bonecrusherCharges by intVarBit("varbit.charges_bonecrusher_quantity") +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltBonecrusherUnlock.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltBonecrusherUnlock.kt new file mode 100644 index 000000000..8b43dad6e --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltBonecrusherUnlock.kt @@ -0,0 +1,87 @@ +package org.rsmod.content.other.toolbelt + +import org.rsmod.api.player.protect.ProtectedAccess +import org.rsmod.api.player.stat.statBase +import org.rsmod.api.table.ToolbeltSlotsRow +import org.rsmod.game.entity.Player +import org.rsmod.game.inv.InvObj +import org.rsmod.game.type.getInvObj + +object ToolbeltBonecrusherUnlock { + const val ECTO_TOKEN = "obj.ectotoken" + const val ECTO_TOKEN_COST = 2000 + + fun bonecrusherSlot(): ToolbeltSlotsRow? = Toolbelt.slotFor(ToolbeltBonecrusher.ITEM) + + fun canOfferUnlock(player: Player): Boolean { + if (!Toolbelt.isEnabled()) { + return false + } + val slot = bonecrusherSlot() ?: return false + if (ToolbeltUnlocks.isUnlocked(player, slot)) { + return false + } + if (!meetsSlotSkillRequirement(player, slot)) { + return false + } + return playerCarriesBonecrusher(player) + } + + fun playerCarriesBonecrusher(player: Player): Boolean { + if (player.inv.any { it.isBonecrusher() }) { + return true + } + return player.worn.any { it.isBonecrusher() } + } + + fun tryUnlock(access: ProtectedAccess): PayResult { + if (!Toolbelt.isEnabled()) { + return PayResult.NotEnabled + } + val slot = bonecrusherSlot() ?: return PayResult.NoSlot + if (ToolbeltUnlocks.isUnlocked(access.player, slot)) { + return PayResult.AlreadyUnlocked + } + if (!meetsSlotSkillRequirement(access.player, slot)) { + val required = slot.requireLevel ?: 1 + return PayResult.InsufficientPrayer(required) + } + if (!playerCarriesBonecrusher(access.player)) { + return PayResult.NeedsBonecrusher + } + val tokens = access.invContentTotal(access.inv, ECTO_TOKEN) + if (tokens < ECTO_TOKEN_COST) { + return PayResult.InsufficientTokens(tokens) + } + if (access.invDel(access.inv, ECTO_TOKEN, ECTO_TOKEN_COST).failure) { + return PayResult.InsufficientTokens(tokens) + } + ToolbeltUnlocks.setUnlocked(access.player, slot, unlocked = true) + return PayResult.Success + } + + private fun meetsSlotSkillRequirement(player: Player, slot: ToolbeltSlotsRow): Boolean { + val level = slot.requireLevel ?: return true + val skill = slot.skillStat ?: return true + return player.statBase(skill.internalName) >= level + } + + sealed class PayResult { + data object Success : PayResult() + + data object NotEnabled : PayResult() + + data object NoSlot : PayResult() + + data object AlreadyUnlocked : PayResult() + + data object NeedsBonecrusher : PayResult() + + data class InsufficientPrayer(val required: Int) : PayResult() + + data class InsufficientTokens(val held: Int) : PayResult() + } +} + +private fun InvObj?.isBonecrusher(): Boolean = + this != null && getInvObj(this).internalName == ToolbeltBonecrusher.ITEM diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltModule.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltModule.kt new file mode 100644 index 000000000..51e247678 --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltModule.kt @@ -0,0 +1,10 @@ +package org.rsmod.content.other.toolbelt + +import org.rsmod.api.inv.storage.PlayerItemStorageHook +import org.rsmod.plugin.module.PluginModule + +class ToolbeltModule : PluginModule() { + override fun bind() { + addSetBinding(ToolbeltStorageHook::class.java) + } +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltScript.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltScript.kt new file mode 100644 index 000000000..a1c6bf93f --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltScript.kt @@ -0,0 +1,461 @@ +package org.rsmod.content.other.toolbelt + +import dev.openrune.ServerCacheManager +import dev.openrune.definition.type.widget.IfEvent +import dev.openrune.rscm.RSCM.asRSCM +import dev.openrune.rscm.RSCMType +import dev.openrune.types.aconverted.interf.IfButtonOp +import jakarta.inject.Inject +import org.rsmod.api.invtx.invAdd +import org.rsmod.api.invtx.invDel +import org.rsmod.api.player.output.mes +import org.rsmod.api.player.protect.ProtectedAccess +import org.rsmod.api.player.output.objExamine +import org.rsmod.api.player.vars.resyncVar +import org.rsmod.api.script.onCommand +import org.rsmod.api.script.onIfOpen +import org.rsmod.api.script.onIfModalButton +import org.rsmod.api.script.onIfOverlayButton +import org.rsmod.api.script.onOpHeld1 +import org.rsmod.api.script.onOpHeld2 +import org.rsmod.api.script.onOpHeld3 +import org.rsmod.api.script.onOpHeld4 +import org.rsmod.api.script.onPlayerInit +import org.rsmod.api.table.ToolbeltSlotsRow +import org.rsmod.events.EventBus +import org.rsmod.game.cheat.Cheat +import org.rsmod.game.entity.Player +import org.rsmod.game.type.getInvObj +import org.rsmod.plugin.scripts.PluginScript +import org.rsmod.plugin.scripts.ScriptContext + +class ToolbeltScript @Inject constructor( + val eventBus: EventBus, +) : PluginScript() { + override fun ScriptContext.startup() { + onIfOpen("interface.wornitems") { + ToolbeltWornItems.sync(player) + } + + onPlayerInit { + if (Toolbelt.isEnabled()) { + Toolbelt.validateStorageOnLogin(player) + } + ToolbeltWornItems.sync(player) + } + + onIfOverlayButton("component.wornitems:tb_toolbelt") { + if (Toolbelt.isEnabled()) { + openToolbelt() + } + } + + if (!Toolbelt.isEnabled()) { + return + } + + installAddOptions() + onIfModalButton(ToolbeltUi.CONTENT) { contentOp(it.comsub, it.op) } + onCommand("toolbelt") { + desc = "Tool belt: list|add|grant|remove|unlock|lock" + invalidArgs = + "Usage: ::toolbelt list | add | grant | remove | unlock|lock " + cheat { handleToolbeltCommand() } + } + } + + private fun ProtectedAccess.openToolbelt() { + player.syncToolbeltVarsToClient() + ifOpenMainModal(ToolbeltUi.INTERFACE) + ifSetEvents( + ToolbeltUi.CONTENT, + ToolbeltUi.ITEM_COM_BASE until (ToolbeltUi.ITEM_COM_BASE + ToolbeltUi.SLOT_COUNT), + IfEvent.Op1, + IfEvent.Op2, + IfEvent.Op3, + IfEvent.Op4, + IfEvent.Op5, + IfEvent.Op10, + ) + ifSetEvents( + ToolbeltUi.CONTENT, + ToolbeltUi.LOCK_COM_BASE until (ToolbeltUi.LOCK_COM_BASE + ToolbeltUi.SLOT_COUNT), + IfEvent.Op1, + ) + } + + private fun Player.syncToolbeltVarsToClient() { + resyncVar("varp.toolbelt_unlocks") + for (slot in 0 until ToolbeltVars.SLOT_COUNT) { + resyncVar(ToolbeltVars.varpName(slot)) + } + } + + private suspend fun ProtectedAccess.contentOp(comsub: Int, op: IfButtonOp) { + val slotIndex = ToolbeltUi.slotFromComsub(comsub) ?: return + val slot = + ToolbeltSlotsRow.all().firstOrNull { it.slotIndex == slotIndex } ?: return + if (!ToolbeltUnlocks.isUnlocked(player, slot)) { + when (op) { + IfButtonOp.Op1 -> showSlotInfo(slot) + IfButtonOp.Op10 -> examineSlot(slot) + else -> Unit + } + return + } + when (op) { + IfButtonOp.Op10 -> examineSlot(slot) + IfButtonOp.Op1 -> primarySlotOp(slot) + IfButtonOp.Op2, + IfButtonOp.Op3, + IfButtonOp.Op4, + IfButtonOp.Op5, + -> bonecrusherSlotOp(slot, op) + else -> Unit + } + } + + private suspend fun ProtectedAccess.bonecrusherSlotOp(slot: ToolbeltSlotsRow, op: IfButtonOp) { + if (slot.slotIndex != ToolbeltUi.BONE_CRUSHER_SLOT || !Toolbelt.has(player, slot)) { + return + } + when (op) { + IfButtonOp.Op2 -> ToolbeltBonecrusher.checkCharges(this) + IfButtonOp.Op3 -> ToolbeltBonecrusher.charge(this) + IfButtonOp.Op4 -> ToolbeltBonecrusher.toggleActivity(this) + IfButtonOp.Op5 -> ToolbeltBonecrusher.uncharge(this) + else -> Unit + } + } + + private fun ProtectedAccess.examineSlot(slot: ToolbeltSlotsRow) { + val type = Toolbelt.displayType(player, slot) + player.objExamine(type, Toolbelt.OWNED_COUNT, 0) + } + + private fun ProtectedAccess.primarySlotOp(slot: ToolbeltSlotsRow) { + if (Toolbelt.has(player, slot)) { + removeFromSlot(slot) + } + } + + private suspend fun ProtectedAccess.showSlotInfo(slot: ToolbeltSlotsRow) { + ToolbeltSlotInfo.show(this, slot, eventBus) + } + + private fun ProtectedAccess.removeFromSlot(slot: ToolbeltSlotsRow) { + val removed = Toolbelt.tryRemove(player, slot) + if (removed == null) { + mes("You can't remove that from your tool belt.") + return + } + val given = + invAdd( + inv, + removed.internalName, + 1, + strict = false, + ignoreVirtualStorage = true, + ) + if (given.completed() <= 0) { + Toolbelt.tryAdd(player, removed) + mes("You need inventory space to remove that from your tool belt.") + return + } + mes("You remove the ${removed.name} from your tool belt.") + } + + private fun ScriptContext.installAddOptions() { + for (type in Toolbelt.allEligibleItems()) { + val slotIndex = type.interfaceOptions.indexOfFirst { it == Toolbelt.ADD_OPTION } + if (slotIndex < 0) { + continue + } + val internal = type.internalName + when (slotIndex) { + 0 -> onOpHeld1(internal) { addFromInventory(it.slot) } + 1 -> onOpHeld2(internal) { addFromInventory(it.slot) } + 2 -> onOpHeld3(internal) { addFromInventory(it.slot) } + 3 -> onOpHeld4(internal) { addFromInventory(it.slot) } + } + } + } + + private fun ProtectedAccess.addFromInventory(invSlot: Int) { + val obj = inv[invSlot] ?: return + val type = getInvObj(obj) + val slot = Toolbelt.slotFor(type) + if (slot == null) { + mes("You can't add that to your tool belt.") + return + } + + when (val result = Toolbelt.tryAdd(player, type)) { + is Toolbelt.AddResult.Failed -> mes(result.message) + is Toolbelt.AddResult.Added -> { + if ( + invDel(inv, type.internalName, 1, slot = invSlot, ignoreVirtualStorage = true) + .failure + ) { + Toolbelt.tryRemove(player, slot) + mes("You don't have enough inventory space.") + return + } + mes("You add the ${type.name} to your tool belt.") + player.syncToolbeltVarsToClient() + } + is Toolbelt.AddResult.Replaced -> { + if ( + invDel(inv, type.internalName, 1, slot = invSlot, ignoreVirtualStorage = true) + .failure + ) { + Toolbelt.tryAdd(player, result.returned) + mes("You don't have enough inventory space.") + return + } + val given = + invAdd( + inv, + result.returned.internalName, + 1, + strict = false, + ignoreVirtualStorage = true, + ) + if (given.completed() <= 0) { + invAdd(inv, type.internalName, 1, strict = false, ignoreVirtualStorage = true) + Toolbelt.tryAdd(player, result.returned) + mes("You don't have enough inventory space to swap tools.") + return + } + mes( + "You add the ${result.added.name} to your tool belt and receive your ${result.returned.name} back.", + ) + player.syncToolbeltVarsToClient() + } + } + } + + private fun Cheat.handleToolbeltCommand() { + when (args.getOrNull(0)?.lowercase()) { + null, + "list" -> listToolbelt() + "add" -> addFromFirstSlot() + "grant" -> grantToSlot() + "remove", + "take", -> removeSlot() + "unlock" -> setSlotUnlock(locked = false) + "lock" -> setSlotUnlock(locked = true) + else -> + player.mes( + "Usage: ::toolbelt list | add | grant | remove | unlock|lock ", + ) + } + } + + private fun Cheat.setSlotUnlock(locked: Boolean) { + val key = args.getOrNull(1) + if (key.isNullOrBlank()) { + player.mes("Usage: ::toolbelt ${if (locked) "lock" else "unlock"} ") + listUnlockableSlots() + return + } + val slot = slotArg(1) + if (slot == null) { + player.mes("Unknown slot '${args.getOrNull(1)}'.") + return + } + if (slot.unlockBit == null) { + player.mes("The ${slot.displayName} slot is not lockable.") + return + } + ToolbeltUnlocks.setUnlocked(player, slot, unlocked = !locked) + player.mes( + if (locked) { + "Locked the ${slot.displayName.lowercase()} tool belt slot." + } else { + "Unlocked the ${slot.displayName.lowercase()} tool belt slot." + }, + ) + } + + private fun Cheat.listUnlockableSlots() { + val unlockable = ToolbeltUnlocks.slotsWithUnlock() + if (unlockable.isEmpty()) { + return + } + player.mes("Unlockable slots: " + unlockable.joinToString(", ") { "${it.slotIndex} ${it.displayName}" }) + } + + private fun Cheat.listToolbelt() { + val occupied = Toolbelt.occupiedSlots(player) + if (occupied.isEmpty()) { + player.mes("Your tool belt is empty.") + return + } + player.mes("Tool belt contents:") + for ((slot, type) in occupied) { + player.mes("- ${slot.displayName}: ${type.name}") + } + } + + private fun Cheat.grantToSlot() { + val arg = args.getOrNull(1)?.trim().orEmpty() + if (arg.isEmpty()) { + player.mes("Usage: ::toolbelt grant ") + return + } + val slotIndex = arg.toIntOrNull() + val slot = slotArg(1) + val type = + when { + slotIndex != null && slot != null -> slot.defaultItem + else -> { + val name = if (arg.startsWith("obj.")) arg else "obj.$arg" + ServerCacheManager.getItem(name.asRSCM(RSCMType.OBJ)) + } + } + if (type == null) { + player.mes("Unknown item or slot '$arg'.") + return + } + val resolvedSlot = Toolbelt.slotFor(type) + if (resolvedSlot == null) { + player.mes("That item cannot go on the tool belt.") + return + } + when (val result = Toolbelt.grantOwned(player, type, bypassRequirements = true)) { + is Toolbelt.AddResult.Failed -> player.mes(result.message) + is Toolbelt.AddResult.Added -> { + player.mes("Granted ${type.name} to your tool belt.") + player.syncToolbeltVarsToClient() + } + is Toolbelt.AddResult.Replaced -> { + player.mes("Granted ${type.name} (replaced ${result.returned.name}).") + player.syncToolbeltVarsToClient() + } + } + } + + private fun Cheat.addFromFirstSlot() { + val invSlot = + player.inv.indexOfFirst { obj -> + obj != null && Toolbelt.slotFor(getInvObj(obj)) != null + } + if (invSlot < 0) { + player.mes("You need a toolbelt-compatible tool in your inventory.") + return + } + val obj = player.inv[invSlot] ?: return + val type = getInvObj(obj) + val slot = Toolbelt.slotFor(type) ?: return + + when (val result = Toolbelt.tryAdd(player, type)) { + is Toolbelt.AddResult.Failed -> player.mes(result.message) + is Toolbelt.AddResult.Added -> { + if ( + player + .invDel( + player.inv, + type.internalName, + 1, + slot = invSlot, + ignoreVirtualStorage = true, + ) + .failure + ) { + Toolbelt.tryRemove(player, slot) + player.mes("Failed to remove the tool from your inventory.") + return + } + player.mes("You add the ${type.name} to your tool belt.") + } + is Toolbelt.AddResult.Replaced -> { + if ( + player + .invDel( + player.inv, + type.internalName, + 1, + slot = invSlot, + ignoreVirtualStorage = true, + ) + .failure + ) { + Toolbelt.tryAdd(player, result.returned) + player.mes("Failed to remove the tool from your inventory.") + return + } + val given = + player.invAdd( + player.inv, + result.returned.internalName, + 1, + strict = false, + ignoreVirtualStorage = true, + ) + if (given.completed() <= 0) { + player.invAdd( + player.inv, + type.internalName, + 1, + strict = false, + ignoreVirtualStorage = true, + ) + Toolbelt.tryAdd(player, result.returned) + player.mes("You don't have enough inventory space to swap tools.") + return + } + player.mes( + "You add the ${result.added.name} to your tool belt and receive your ${result.returned.name} back.", + ) + } + } + } + + private fun Cheat.removeSlot() { + val key = args.getOrNull(1) + if (key.isNullOrBlank()) { + player.mes("Usage: ::toolbelt remove ") + player.mes( + "Slots: " + + ToolbeltSlotsRow.all().sortedBy { it.slotIndex }.joinToString { "${it.slotIndex}=${it.displayName}" }, + ) + return + } + val slot = slotArg(1) + if (slot == null) { + player.mes("Unknown slot '${args.getOrNull(1)}'.") + return + } + val removed = Toolbelt.tryRemove(player, slot) + if (removed == null) { + player.mes("Your tool belt does not have a ${slot.displayName.lowercase()}.") + return + } + val given = + player.invAdd( + player.inv, + removed.internalName, + 1, + strict = false, + ignoreVirtualStorage = true, + ) + if (given.completed() <= 0) { + Toolbelt.tryAdd(player, removed) + player.mes("You need inventory space to remove that from your tool belt.") + return + } + player.mes("You remove the ${removed.name} from your tool belt.") + } + + private fun Cheat.slotArg(index: Int): ToolbeltSlotsRow? { + val arg = args.getOrNull(index)?.trim().orEmpty() + if (arg.isEmpty()) { + return null + } + arg.toIntOrNull()?.let { slotIndex -> + return ToolbeltSlotsRow.all().firstOrNull { it.slotIndex == slotIndex } + } + return Toolbelt.slotFor(arg) + } +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSettings.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSettings.kt new file mode 100644 index 000000000..8e49a3afb --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSettings.kt @@ -0,0 +1,34 @@ +package org.rsmod.content.other.toolbelt + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.toml.TomlFactory +import com.fasterxml.jackson.module.kotlin.readValue +import com.fasterxml.jackson.module.kotlin.registerKotlinModule + +@JsonIgnoreProperties(ignoreUnknown = true) +data class ToolbeltSettings( + @JsonProperty("is_enabled") val isEnabled: Boolean = true, + @JsonProperty("allow_higher_tier_unlock") val allowHigherTierUnlock: Boolean = true, +) { + companion object { + private const val RESOURCE = "toolbelt.toml" + + private val mapper: ObjectMapper = ObjectMapper(TomlFactory()).registerKotlinModule() + + @Volatile private var cached: ToolbeltSettings? = null + + fun load(): ToolbeltSettings { + cached?.let { + return it + } + val stream = + ToolbeltSettings::class.java.classLoader.getResourceAsStream(RESOURCE) + ?: error("Missing plugin config resource: $RESOURCE") + return stream.use { input -> + mapper.readValue(input).also { cached = it } + } + } + } +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSlotInfo.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSlotInfo.kt new file mode 100644 index 000000000..fcc62c007 --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSlotInfo.kt @@ -0,0 +1,68 @@ +package org.rsmod.content.other.toolbelt + +import dev.openrune.rscm.RSCM.asRSCM +import dev.openrune.rscm.RSCMType +import dev.openrune.definition.type.widget.IfEvent +import org.rsmod.api.config.constants +import org.rsmod.api.player.output.runClientScript +import org.rsmod.api.player.protect.ProtectedAccess +import org.rsmod.api.player.ui.ifCloseOverlay +import org.rsmod.api.player.ui.ifSetEvents +import org.rsmod.api.player.vars.intVarBit +import org.rsmod.api.table.ToolbeltSlotsRow +import org.rsmod.events.EventBus +import org.rsmod.game.entity.Player + +object ToolbeltSlotInfo { + private const val HIGHLIGHT = "component.toolbelt:highlight" + private const val SCREEN_HIGHLIGHT = "interface.screenhighlight" + + private const val CS_HIGHLIGHT_START = 3408 + private const val CS_HIGHLIGHT_SCREEN = 3409 + private const val CS_HIGHLIGHT_TEXTBOX = 3413 + private const val CS_HIGHLIGHT_HIDE = 3412 + + private var Player.tutorialPage by intVarBit("varbit.hnt_hint_step") + private var Player.tutorialTotalPages by intVarBit("varbit.hnt_hint_max_step") + + suspend fun show(access: ProtectedAccess, slot: ToolbeltSlotsRow, eventBus: EventBus) { + val player = access.player + val text = ToolbeltUnlocks.unlockMessage(slot) + val highlight = HIGHLIGHT.asRSCM(RSCMType.COMPONENT) + + player.tutorialTotalPages = 1 + player.tutorialPage = 0 + + player.runClientScript(CS_HIGHLIGHT_START, highlight) + access.ifOpenOverlay(SCREEN_HIGHLIGHT, HIGHLIGHT) + player.runClientScript( + CS_HIGHLIGHT_SCREEN, + highlight, + 0, + 0, + 0, + 0, + constants.setpos_abs_right, + constants.setpos_abs_bottom, + 100, + 1, + ) + player.runClientScript( + CS_HIGHLIGHT_TEXTBOX, + text, + -1, + 1, + 0, + highlight, + ) + access.ifSetEvents("component.screenhighlight:continue", 9..9, IfEvent.Op1) + access.ifSetEvents("component.screenhighlight:previous", 9..9, IfEvent.Op1) + access.ifSetEvents("component.screenhighlight:pausebutton", -1..-1, IfEvent.PauseButton) + try { + access.pauseButton() + } finally { + player.runClientScript(CS_HIGHLIGHT_HIDE, highlight) + player.ifCloseOverlay(SCREEN_HIGHLIGHT, eventBus) + } + } +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSlots.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSlots.kt new file mode 100644 index 000000000..f05dd5d6d --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltSlots.kt @@ -0,0 +1,13 @@ +package org.rsmod.content.other.toolbelt + +import dev.openrune.types.ItemServerType +import org.rsmod.api.table.ToolbeltSlotsRow + +val ToolbeltSlotsRow.defaultItem: ItemServerType + get() = allowedItems.first() + +fun ToolbeltSlotsRow.accepts(internalName: String): Boolean = + allowedItems.any { it.internalName == internalName } + +fun ToolbeltSlotsRow.accepts(type: ItemServerType): Boolean = + allowedItems.any { it.id == type.id } diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltStorageHook.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltStorageHook.kt new file mode 100644 index 000000000..7ac3efd96 --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltStorageHook.kt @@ -0,0 +1,26 @@ +package org.rsmod.content.other.toolbelt + +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.rsmod.api.inv.storage.PlayerItemStorageContext +import org.rsmod.api.inv.storage.PlayerItemStorageHook +import org.rsmod.api.inv.storage.VirtualItemConsumePolicy +import org.rsmod.game.entity.Player + +@Singleton +internal class ToolbeltStorageHook @Inject constructor() : PlayerItemStorageHook { + override val consumePolicy: VirtualItemConsumePolicy = VirtualItemConsumePolicy.InventoryFirst + + override fun shouldProcess(ctx: PlayerItemStorageContext): Boolean = + Toolbelt.isEnabled() && Toolbelt.provides(ctx.player, ctx.itemInternal) + + override fun contains(ctx: PlayerItemStorageContext): Int = + if (Toolbelt.provides(ctx.player, ctx.itemInternal)) 1 else 0 + + override fun remove(ctx: PlayerItemStorageContext, amount: Int): Int = 0 + + override fun add(ctx: PlayerItemStorageContext, amount: Int): Int = 0 + + override fun providesContentGroup(player: Player, contentGroup: Int): Boolean = + Toolbelt.providesContentGroup(player, contentGroup) +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltUi.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltUi.kt new file mode 100644 index 000000000..46552bf5a --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltUi.kt @@ -0,0 +1,17 @@ +package org.rsmod.content.other.toolbelt + +object ToolbeltUi { + const val INTERFACE = "interface.toolbelt" + const val CONTENT = "component.toolbelt:content" + const val SLOT_COUNT = 28 + const val ITEM_COM_BASE = 300 + const val LOCK_COM_BASE = 400 + const val BONE_CRUSHER_SLOT = 27 + + fun slotFromComsub(comsub: Int): Int? = when (comsub) { + in ITEM_COM_BASE until (ITEM_COM_BASE + SLOT_COUNT) -> comsub - ITEM_COM_BASE + in LOCK_COM_BASE until (LOCK_COM_BASE + SLOT_COUNT) -> comsub - LOCK_COM_BASE + in 0 until SLOT_COUNT -> comsub + else -> null + } +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltUnlocks.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltUnlocks.kt new file mode 100644 index 000000000..ef79cc602 --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltUnlocks.kt @@ -0,0 +1,39 @@ +package org.rsmod.content.other.toolbelt + +import dev.openrune.ServerCacheManager +import dev.openrune.rscm.RSCM.asRSCM +import dev.openrune.rscm.RSCMType +import org.rsmod.api.player.vars.VarPlayerIntMapSetter +import org.rsmod.api.table.ToolbeltSlotsRow +import org.rsmod.game.entity.Player + +object ToolbeltUnlocks { + private const val UNLOCK_VARP = "varp.toolbelt_unlocks" + + fun isUnlocked(player: Player, slot: ToolbeltSlotsRow): Boolean { + val bit = slot.unlockBit ?: return true + val varp = ServerCacheManager.getVarp(UNLOCK_VARP.asRSCM(RSCMType.VARP)) ?: return true + return player.vars[varp] and (1 shl bit) != 0 + } + + fun setUnlocked(player: Player, slot: ToolbeltSlotsRow, unlocked: Boolean) { + val bit = slot.unlockBit ?: return + val varp = ServerCacheManager.getVarp(UNLOCK_VARP.asRSCM(RSCMType.VARP)) ?: return + val flag = 1 shl bit + val current = player.vars[varp] + val value = + if (unlocked) { + current or flag + } else { + current and flag.inv() + } + VarPlayerIntMapSetter.set(player, varp, value) + } + + fun unlockMessage(slot: ToolbeltSlotsRow): String = + slot.unlockHint?.takeIf { it.isNotBlank() } + ?: "You cannot add this tool to your tool belt yet." + + fun slotsWithUnlock(): List = + ToolbeltSlotsRow.all().filter { it.unlockBit != null } +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltVars.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltVars.kt new file mode 100644 index 000000000..1335f8f46 --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltVars.kt @@ -0,0 +1,31 @@ +package org.rsmod.content.other.toolbelt + +import org.rsmod.api.player.vars.VarPlayerIntMapSetter +import org.rsmod.game.entity.Player + +object ToolbeltVars { + const val EMPTY_STORED = 0 + const val SLOT_COUNT = 28 + + fun varBitName(slotIndex: Int): String { + require(slotIndex in 0 until SLOT_COUNT) { "Invalid toolbelt slot: $slotIndex" } + return "varbit.toolbelt_slot_$slotIndex" + } + + fun varpName(slotIndex: Int): String { + require(slotIndex in 0 until SLOT_COUNT) { "Invalid toolbelt slot: $slotIndex" } + return "varp.toolbelt_slot_$slotIndex" + } + + fun readStoredAllowedIndex(player: Player, slotIndex: Int): Int = + player.vars[varBitName(slotIndex)] + + fun setStoredAllowedIndex(player: Player, slotIndex: Int, allowedIndex: Int) { + val value = if (allowedIndex <= EMPTY_STORED) EMPTY_STORED else allowedIndex + VarPlayerIntMapSetter.set(player, varBitName(slotIndex), value) + } + + fun clearSlot(player: Player, slotIndex: Int) { + setStoredAllowedIndex(player, slotIndex, EMPTY_STORED) + } +} diff --git a/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltWornItems.kt b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltWornItems.kt new file mode 100644 index 000000000..57b220f2e --- /dev/null +++ b/content/other/toolbelt/src/main/kotlin/org/rsmod/content/other/toolbelt/ToolbeltWornItems.kt @@ -0,0 +1,18 @@ +package org.rsmod.content.other.toolbelt + +import org.rsmod.api.player.ui.ifSetHide +import org.rsmod.content.other.toolbelt.pack.WORN_FOOTER_TOOLBELT_ROW +import org.rsmod.content.other.toolbelt.pack.WORN_FOOTER_VANILLA +import org.rsmod.game.entity.Player + +object ToolbeltWornItems { + fun sync(player: Player) { + val enabled = Toolbelt.isEnabled() + for (name in WORN_FOOTER_VANILLA) { + player.ifSetHide(name, hide = enabled) + } + for (name in WORN_FOOTER_TOOLBELT_ROW) { + player.ifSetHide(name, hide = !enabled) + } + } +} diff --git a/content/other/toolbelt/src/main/resources/gamevals.toml b/content/other/toolbelt/src/main/resources/gamevals.toml new file mode 100644 index 000000000..98337d8ea --- /dev/null +++ b/content/other/toolbelt/src/main/resources/gamevals.toml @@ -0,0 +1,114 @@ +[gamevals.interface] +toolbelt = 15000 + +[gamevals.clientscript] +toolbelt_init = 45400 +toolbelt_build = 45401 +toolbelt_stored_for_row = 45402 +toolbelt_draw = 45403 +toolbelt_count_category = 45404 +toolbelt_filter = 45405 + +[gamevals.dbtable] +toolbelt_slots = 55521 + +[gamevals.enum] +toolbelt_categories = 65541 + +[gamevals.dbrow] +toolbelt_pickaxe =64130 +toolbelt_axe =64129 +toolbelt_hammer =64128 +toolbelt_chisel =64127 +toolbelt_knife =64126 +toolbelt_tinderbox =64125 +toolbelt_pestle_and_mortar =64124 +toolbelt_spade =64123 +toolbelt_rake =64122 +toolbelt_dibber =64121 +toolbelt_secateurs =64120 +toolbelt_gardening_trowel =64119 +toolbelt_watering_can =64118 +toolbelt_shears =64117 +toolbelt_needle =64116 +toolbelt_glassblowing_pipe =64115 +toolbelt_small_net =64114 +toolbelt_big_net =64113 +toolbelt_fishing_rod =64112 +toolbelt_fly_fishing_rod =64111 +toolbelt_harpoon =64110 +toolbelt_lobster_pot =64109 +toolbelt_amulet_mould =64108 +toolbelt_necklace_mould =64107 +toolbelt_ring_mould =64106 +toolbelt_bracelet_mould =64105 +toolbelt_tiara_mould =64104 +toolbelt_bonecrusher =64103 + +[gamevals.varp] +toolbelt_unlocks =65486 +toolbelt_slot_0 =65480 +toolbelt_slot_1 =65479 +toolbelt_slot_2 =65478 +toolbelt_slot_3 =65477 +toolbelt_slot_4 =65476 +toolbelt_slot_5 =65475 +toolbelt_slot_6 =65474 +toolbelt_slot_7 =65473 +toolbelt_slot_8 =65472 +toolbelt_slot_9 =65471 +toolbelt_slot_10 =65470 +toolbelt_slot_11 =65469 +toolbelt_slot_12 =65468 +toolbelt_slot_13 =65467 +toolbelt_slot_14 =65466 +toolbelt_slot_15 =65465 +toolbelt_slot_16 =65464 +toolbelt_slot_17 =65463 +toolbelt_slot_18 =65462 +toolbelt_slot_19 =65461 +toolbelt_slot_20 =65460 +toolbelt_slot_21 =65459 +toolbelt_slot_22 =65458 +toolbelt_slot_23 =65457 +toolbelt_slot_24 =65456 +toolbelt_slot_25 =65455 +toolbelt_slot_26 =65454 +toolbelt_slot_27 =65453 + +[gamevals.varbit] +toolbelt_slot_0 =65454 +toolbelt_slot_1 =65453 +toolbelt_slot_2 =65452 +toolbelt_slot_3 =65451 +toolbelt_slot_4 =65450 +toolbelt_slot_5 =65449 +toolbelt_slot_6 =65448 +toolbelt_slot_7 =65447 +toolbelt_slot_8 =65446 +toolbelt_slot_9 =65445 +toolbelt_slot_10 =65444 +toolbelt_slot_11 =65443 +toolbelt_slot_12 =65442 +toolbelt_slot_13 =65441 +toolbelt_slot_14 =65440 +toolbelt_slot_15 =65439 +toolbelt_slot_16 =65438 +toolbelt_slot_17 =65437 +toolbelt_slot_18 =65436 +toolbelt_slot_19 =65435 +toolbelt_slot_20 =65434 +toolbelt_slot_21 =65433 +toolbelt_slot_22 =65432 +toolbelt_slot_23 =65431 +toolbelt_slot_24 =65430 +toolbelt_slot_25 =65429 +toolbelt_slot_26 =65428 +toolbelt_slot_27 =65427 + +[gamevals.sprites] +tool_belt = 21535 +square_button = 21536 +square_button_hover = 21537 +square_button_clicked = 21538 +square_button_disabled = 21539 \ No newline at end of file diff --git a/content/other/toolbelt/src/main/resources/pack/configs/enum.toml b/content/other/toolbelt/src/main/resources/pack/configs/enum.toml new file mode 100644 index 000000000..d907cdf80 --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/configs/enum.toml @@ -0,0 +1,11 @@ +[[enum]] +id = "enum.toolbelt_categories" +keyType = "INT" +valueType = "STRING" + +[enum.values] +0 = "General" +1 = "Farming" +2 = "Fishing" +3 = "Crafting" +4 = "Miscellaneous" diff --git a/content/other/toolbelt/src/main/resources/pack/configs/varbit.toml b/content/other/toolbelt/src/main/resources/pack/configs/varbit.toml new file mode 100644 index 000000000..1d25b5c1c --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/configs/varbit.toml @@ -0,0 +1,168 @@ +[[varbit]] +id = "varbit.toolbelt_slot_0" +varp = "varp.toolbelt_slot_0" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_1" +varp = "varp.toolbelt_slot_1" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_2" +varp = "varp.toolbelt_slot_2" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_3" +varp = "varp.toolbelt_slot_3" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_4" +varp = "varp.toolbelt_slot_4" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_5" +varp = "varp.toolbelt_slot_5" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_6" +varp = "varp.toolbelt_slot_6" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_7" +varp = "varp.toolbelt_slot_7" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_8" +varp = "varp.toolbelt_slot_8" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_9" +varp = "varp.toolbelt_slot_9" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_10" +varp = "varp.toolbelt_slot_10" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_11" +varp = "varp.toolbelt_slot_11" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_12" +varp = "varp.toolbelt_slot_12" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_13" +varp = "varp.toolbelt_slot_13" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_14" +varp = "varp.toolbelt_slot_14" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_15" +varp = "varp.toolbelt_slot_15" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_16" +varp = "varp.toolbelt_slot_16" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_17" +varp = "varp.toolbelt_slot_17" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_18" +varp = "varp.toolbelt_slot_18" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_19" +varp = "varp.toolbelt_slot_19" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_20" +varp = "varp.toolbelt_slot_20" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_21" +varp = "varp.toolbelt_slot_21" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_22" +varp = "varp.toolbelt_slot_22" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_23" +varp = "varp.toolbelt_slot_23" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_24" +varp = "varp.toolbelt_slot_24" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_25" +varp = "varp.toolbelt_slot_25" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_26" +varp = "varp.toolbelt_slot_26" +startBit = 0 +endBit = 15 + +[[varbit]] +id = "varbit.toolbelt_slot_27" +varp = "varp.toolbelt_slot_27" +startBit = 0 +endBit = 15 + diff --git a/content/other/toolbelt/src/main/resources/pack/configs/varp.toml b/content/other/toolbelt/src/main/resources/pack/configs/varp.toml new file mode 100644 index 000000000..c4084603c --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/configs/varp.toml @@ -0,0 +1,115 @@ +[[varp]] +id = "varp.toolbelt_unlocks" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_0" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_1" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_2" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_3" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_4" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_5" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_6" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_7" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_8" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_9" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_10" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_11" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_12" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_13" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_14" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_15" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_16" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_17" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_18" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_19" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_20" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_21" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_22" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_23" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_24" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_25" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_26" +scope = "Perm" + +[[varp]] +id = "varp.toolbelt_slot_27" +scope = "Perm" diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/script/[clientscript,toolbelt_filter].cs2 b/content/other/toolbelt/src/main/resources/pack/cs2/script/[clientscript,toolbelt_filter].cs2 new file mode 100644 index 000000000..7adaead43 --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/script/[clientscript,toolbelt_filter].cs2 @@ -0,0 +1,189 @@ +[clientscript,toolbelt_filter](component $content, component $search, component $scrollbar, component $border) +def_string $needle = ""; +def_obj $obj = null; +def_int $slot = 0; +def_int $item = 0; +def_int $y = 0; +def_int $step = 40; +def_boolean $match = false; +def_boolean $any_general = false; +def_boolean $any_farming = false; +def_boolean $any_fishing = false; +def_boolean $any_crafting = false; +def_boolean $any_misc = false; +def_int $cat = 0; +def_int $c = 0; +def_int $header = 0; +def_boolean $any = false; +def_int $cols = 0; +def_int $col = 0; +def_int $bg = 0; +def_int $lock = 0; +def_int $x = 0; +def_dbrow $row = null; +def_obj $ghost = null; +def_boolean $owned = false; +def_int $stored = 0; +def_int $allowed = 0; +def_boolean $locked = false; +if (cc_find($search, 3) = true) { + $needle = lowercase(~string_trim(removetags(cc_gettext))); +} +if (compare($needle, "") = 0) { + ~toolbelt_build($content, $scrollbar); + ~toolbelt_draw($content, $border); + return; +} +while ($slot < 28) { + $match = false; + $item = calc(300 + $slot); + $obj = null; + $ghost = null; + $owned = false; + $row = null; + if (db_find(toolbelt_slots:slot_index, $slot) = 1) { + $row = db_findnext; + if ($row ! null) { + if (db_getfieldcount($row, toolbelt_slots:allowed_items) > 0) { + $ghost = db_getfield($row, toolbelt_slots:allowed_items, 0); + } + } + } + $stored = 0; + if ($row ! null) { + $stored = ~toolbelt_stored_for_row($row); + if ($stored > 0) { + $allowed = db_getfieldcount($row, toolbelt_slots:allowed_items); + if ($stored <= $allowed) { + $obj = db_getfield($row, toolbelt_slots:allowed_items, calc($stored - 1)); + $owned = true; + } + } + } + if ($owned = false) { + $obj = $ghost; + } + if (cc_find($content, $item) = true) { + if ($obj ! null & string_indexof_string(lowercase(oc_name($obj)), $needle, 0) ! -1) { + $match = true; + } + if ($match = true) { + cc_sethide(false); + if (cc_find($content, calc(200 + $slot)) = true) { + cc_sethide(false); + } + $lock = calc(400 + $slot); + $locked = false; + if ($slot = 27 & testbit(%toolbelt_unlocks, 0) = ^false) { + $locked = true; + } + if (cc_find($content, $lock) = true) { + if ($locked = true) { + cc_sethide(false); + } else { + cc_sethide(true); + } + } + if ($row ! null) { + $c = db_getfield($row, toolbelt_slots:category, 0); + if ($c = 0) { + $any_general = true; + } else if ($c = 1) { + $any_farming = true; + } else if ($c = 2) { + $any_fishing = true; + } else if ($c = 3) { + $any_crafting = true; + } else { + $any_misc = true; + } + } + } else { + cc_sethide(true); + if (cc_find($content, calc(200 + $slot)) = true) { + cc_sethide(true); + } + if (cc_find($content, calc(400 + $slot)) = true) { + cc_sethide(true); + } + } + } + $slot = calc($slot + 1); +} +while ($cat < 5) { + $header = calc(100 + $cat); + $cols = ~toolbelt_count_category($cat); + if ($cols > 8) { + $cols = 8; + } + if ($cat = 0) { + $any = $any_general; + } else if ($cat = 1) { + $any = $any_farming; + } else if ($cat = 2) { + $any = $any_fishing; + } else if ($cat = 3) { + $any = $any_crafting; + } else { + $any = $any_misc; + } + if ($any = false) { + if (cc_find($content, $header) = true) { + cc_sethide(true); + } + } else { + if (cc_find($content, $header) = true) { + cc_sethide(false); + cc_setposition(0, $y, ^setpos_abs_left, ^setpos_abs_top); + } + $y = calc($y + 18 + 6); + $col = 0; + $slot = 0; + while ($slot < 28) { + if (db_find(toolbelt_slots:slot_index, $slot) = 1) { + $row = db_findnext; + if ($row ! null & db_getfield($row, toolbelt_slots:category, 0) = $cat) { + $item = calc(300 + $slot); + $bg = calc(200 + $slot); + $lock = calc(400 + $slot); + if (cc_find($content, $item) = true & cc_gethide = false) { + $x = calc($col * $step); + cc_setposition($x, $y, ^setpos_abs_left, ^setpos_abs_top); + if (cc_find($content, $bg) = true) { + cc_sethide(false); + cc_setposition($x, $y, ^setpos_abs_left, ^setpos_abs_top); + } + if (cc_find($content, $lock) = true) { + if ($slot = 27 & testbit(%toolbelt_unlocks, 0) = ^false) { + cc_sethide(false); + cc_setposition(calc($x + 6), calc($y + 2), ^setpos_abs_left, ^setpos_abs_top); + } else { + cc_sethide(true); + } + } + $col = calc($col + 1); + if ($col >= $cols) { + $col = 0; + $y = calc($y + $step); + } + } else if (cc_find($content, $bg) = true) { + cc_sethide(true); + } + } + } + $slot = calc($slot + 1); + } + if ($col ! 0) { + $y = calc($y + $step); + } + $y = calc($y + 6); + } + $cat = calc($cat + 1); +} +if ($y > if_getheight($content)) { + if_setscrollsize(0, $y, $content); + ~scrollbar_vertical($scrollbar, $content, "scrollbar_dragger_v2,3", "scrollbar_dragger_v2,0", "scrollbar_dragger_v2,1", "scrollbar_dragger_v2,2", "scrollbar_v2,0", "scrollbar_v2,1"); +} else { + if_setscrollsize(0, 0, $content); + cc_deleteall($scrollbar); +} diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/script/[clientscript,toolbelt_init].cs2 b/content/other/toolbelt/src/main/resources/pack/cs2/script/[clientscript,toolbelt_init].cs2 new file mode 100644 index 000000000..d152dc9f4 --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/script/[clientscript,toolbelt_init].cs2 @@ -0,0 +1,43 @@ +[clientscript,toolbelt_init](component $border, component $search, component $content, component $scrollbar) +if (cc_find($content, 100) = true) { + ~toolbelt_draw($content, $border); + return; +} +def_component $universe = if_getlayer($border); +if_setposition(0, 0, ^setpos_abs_centre, ^setpos_abs_centre, $universe); +~stoneborder($border, "Toolbelt", 0); +cc_deleteall($search); +cc_create($search, ^iftype_graphic, 0); +cc_setsize(8, 0, ^setsize_minus, ^setsize_minus); +cc_setposition(0, 0, ^setpos_abs_centre, ^setpos_abs_centre); +cc_setgraphic("ge_textbackdrop,1"); +cc_create($search, ^iftype_graphic, 1); +cc_setsize(4, 0, ^setsize_abs, ^setsize_minus); +cc_setposition(0, 0, ^setpos_abs_left, ^setpos_abs_centre); +cc_setgraphic("ge_textbackdrop,0"); +cc_create($search, ^iftype_graphic, 2); +cc_setsize(4, 0, ^setsize_abs, ^setsize_minus); +cc_setposition(0, 0, ^setpos_abs_right, ^setpos_abs_centre); +cc_setgraphic("ge_textbackdrop,2"); +cc_create($search, ^iftype_inputfield, 3); +cc_setposition(4, 4, ^setpos_abs_left, ^setpos_abs_top); +cc_setsize(8, 8, ^setsize_minus, ^setsize_minus); +cc_input_setlinewrappingwidth(cc_getwidth); +cc_input_setacceptmode(0); +cc_settextfont(p12_full); +cc_setcolour(^white); +cc_settextshadow(true); +cc_settextalign(^settextalign_left, ^settextalign_top, 16); +cc_input_setselectcolour(^black); +cc_input_setselectbgcolour(^white); +cc_input_setwrapmode(0); +cc_input_setsubmitmode(0); +cc_input_setlinecountlimit(1); +cc_input_setplaceholdertext("Search tools..."); +cc_input_setplaceholdertextcolour(0x808080); +cc_settext(""); +cc_input_setonchange("toolbelt_filter($content, $search, $scrollbar, $border)"); +cc_input_setonfocuschanged("toolbelt_filter($content, $search, $scrollbar, $border)"); +~toolbelt_build($content, $scrollbar); +~toolbelt_draw($content, $border); +if_setonvartransmit("toolbelt_init($border, $search, $content, $scrollbar){toolbelt_unlocks,toolbelt_slot_0,toolbelt_slot_1,toolbelt_slot_2,toolbelt_slot_3,toolbelt_slot_4,toolbelt_slot_5,toolbelt_slot_6,toolbelt_slot_7,toolbelt_slot_8,toolbelt_slot_9,toolbelt_slot_10,toolbelt_slot_11,toolbelt_slot_12,toolbelt_slot_13,toolbelt_slot_14,toolbelt_slot_15,toolbelt_slot_16,toolbelt_slot_17,toolbelt_slot_18,toolbelt_slot_19,toolbelt_slot_20,toolbelt_slot_21,toolbelt_slot_22,toolbelt_slot_23,toolbelt_slot_24,toolbelt_slot_25,toolbelt_slot_26,toolbelt_slot_27}", $content); diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_build].cs2 b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_build].cs2 new file mode 100644 index 000000000..1eff6101d --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_build].cs2 @@ -0,0 +1,82 @@ +[proc,toolbelt_build](component $content, component $scrollbar) +cc_deleteall($content); +def_int $y = 0; +def_int $step = 40; +def_int $cat = 0; +def_int $cols = 0; +def_int $max = 0; +def_int $col = 0; +def_int $x = 0; +def_int $slot = 0; +def_int $bg = 0; +def_int $item = 0; +def_int $lock = 0; +def_int $header = 0; +def_string $name = ""; +def_string $title = ""; +def_dbrow $row = null; +while ($cat < 5) { + $max = ~toolbelt_count_category($cat); + $header = calc(100 + $cat); + $cols = $max; + if ($cols > 8) { + $cols = 8; + } + $name = enum(int, string, toolbelt_categories, $cat); + $title = "<$name> - 0/"; + cc_create($content, ^iftype_text, $header); + cc_setposition(0, $y, ^setpos_abs_left, ^setpos_abs_top); + cc_setsize(0, 18, ^setsize_minus, ^setsize_abs); + cc_settextfont(b12_full); + cc_settextshadow(true); + cc_settextalign(^settextalign_left, ^settextalign_centre, 0); + cc_setcolour(0xffcb1f); + cc_settext($title); + $y = calc($y + 18 + 6); + $col = 0; + $slot = 0; + while ($slot < 28) { + if (db_find(toolbelt_slots:slot_index, $slot) = 1) { + $row = db_findnext; + if ($row ! null & db_getfield($row, toolbelt_slots:category, 0) = $cat) { + $bg = calc(200 + $slot); + $item = calc(300 + $slot); + $lock = calc(400 + $slot); + $x = calc($col * $step); + cc_create($content, ^iftype_graphic, $bg); + cc_setsize(36, 36, ^setsize_abs, ^setsize_abs); + cc_setposition($x, $y, ^setpos_abs_left, ^setpos_abs_top); + cc_setgraphic(square_button_disabled); + cc_create($content, ^iftype_graphic, $item); + cc_setsize(36, 36, ^setsize_abs, ^setsize_abs); + cc_setposition($x, $y, ^setpos_abs_left, ^setpos_abs_top); + cc_setoutline(1); + cc_setgraphicshadow(0x000000); + cc_setonop("cc_settrans_temporarily(event_com, event_comsubid, 125, 0)"); + cc_create($content, ^iftype_graphic, $lock); + cc_setposition(calc($x + 6), calc($y + 2), ^setpos_abs_left, ^setpos_abs_top); + cc_setgraphic("magic_spell_padlocks,0"); + cc_setsize(30, 30, ^setsize_abs, ^setsize_abs); + cc_sethide(true); + $col = calc($col + 1); + if ($col >= $cols) { + $col = 0; + $y = calc($y + $step); + } + } + } + $slot = calc($slot + 1); + } + if ($col ! 0) { + $y = calc($y + $step); + } + $y = calc($y + 6); + $cat = calc($cat + 1); +} +if ($y > if_getheight($content)) { + if_setscrollsize(0, $y, $content); + ~scrollbar_vertical($scrollbar, $content, "scrollbar_dragger_v2,3", "scrollbar_dragger_v2,0", "scrollbar_dragger_v2,1", "scrollbar_dragger_v2,2", "scrollbar_v2,0", "scrollbar_v2,1"); +} else { + if_setscrollsize(0, 0, $content); + cc_deleteall($scrollbar); +} diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_count_category].cs2 b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_count_category].cs2 new file mode 100644 index 000000000..f530ea5fd --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_count_category].cs2 @@ -0,0 +1,18 @@ +[proc,toolbelt_count_category](int $cat)(int) +def_int $count = 0; +def_int $slot = 0; +def_dbrow $row = null; +def_int $row_cat = 0; +while ($slot < 28) { + if (db_find(toolbelt_slots:slot_index, $slot) = 1) { + $row = db_findnext; + if ($row ! null) { + $row_cat = db_getfield($row, toolbelt_slots:category, 0); + if ($row_cat = $cat) { + $count = calc($count + 1); + } + } + } + $slot = calc($slot + 1); +} +return($count); diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_draw].cs2 b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_draw].cs2 new file mode 100644 index 000000000..dee45b0bd --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_draw].cs2 @@ -0,0 +1,191 @@ +[proc,toolbelt_draw](component $content, component $border) +def_int $slot = 0; +def_int $bg = 0; +def_int $item = 0; +def_int $lock = 0; +def_int $slot_x = 0; +def_int $slot_y = 0; +def_obj $obj = null; +def_obj $ghost = null; +def_boolean $empty = false; +def_boolean $locked = false; +def_boolean $owned = false; +def_int $owned_total = 0; +def_int $general = 0; +def_int $farming = 0; +def_int $fishing = 0; +def_int $crafting = 0; +def_int $misc = 0; +def_int $cat = 0; +def_int $stored = 0; +def_int $allowed = 0; +def_dbrow $row = null; +def_int $dbg_found = 0; +def_int $dbg_ghosted = 0; +def_int $dbg_item_ok = 0; +def_int $dbg_lock_ok = 0; +def_string $owned_s = "n"; +def_string $locked_s = "n"; +def_string $ghost_s = "?"; +def_string $obj_s = "?"; +mes("TB draw: unlocks= s0= s1= s27="); +while ($slot < 28) { + $row = null; + $bg = calc(200 + $slot); + $item = calc(300 + $slot); + $lock = calc(400 + $slot); + $ghost = null; + $obj = null; + $owned = false; + $locked = false; + if ($slot = 27) { + if (testbit(%toolbelt_unlocks, 0) = ^false) { + $locked = true; + } + } + if (db_find(toolbelt_slots:slot_index, $slot) = 1) { + $row = db_findnext; + if ($row ! null) { + $dbg_found = calc($dbg_found + 1); + if (db_getfieldcount($row, toolbelt_slots:allowed_items) > 0) { + $ghost = db_getfield($row, toolbelt_slots:allowed_items, 0); + $dbg_ghosted = calc($dbg_ghosted + 1); + } + } + } + $stored = 0; + if ($row ! null) { + $stored = ~toolbelt_stored_for_row($row); + if ($stored > 0) { + $allowed = db_getfieldcount($row, toolbelt_slots:allowed_items); + if ($stored <= $allowed) { + $obj = db_getfield($row, toolbelt_slots:allowed_items, calc($stored - 1)); + $owned = true; + } + } + } + if ($owned = false) { + $obj = $ghost; + } + if ($obj ! null) { + $empty = true; + if ($owned = true & $locked = false) { + $empty = false; + } + if (cc_find($content, $bg) = true) { + $slot_x = cc_getx; + $slot_y = cc_gety; + cc_sethide(false); + if ($empty = true | $locked = true) { + cc_setgraphic(square_button_disabled); + } else { + cc_setgraphic(square_button); + } + cc_setonmouseover(null); + cc_setonmouseleave(null); + cc_setonop(null); + } + if (cc_find($content, $item) = true) { + $dbg_item_ok = calc($dbg_item_ok + 1); + cc_sethide(false); + cc_setopbase(""); + cc_setop(1, ""); + cc_setop(2, ""); + cc_setop(10, "Examine"); + if ($locked = true) { + cc_setobject_nonum($obj, 1); + cc_setoutline(1); + cc_settrans(140); + cc_setop(1, "Info"); + } else if ($empty = true) { + cc_setobject_nonum($obj, 1); + cc_setoutline(1); + cc_settrans(140); + } else { + cc_setobject($obj, 1); + cc_setoutline(1); + cc_setop(1, "Remove"); + cc_settrans(0); + if ($slot = 27) { + cc_setop(2, "Check"); + cc_setop(3, "Charge"); + cc_setop(4, "Activity"); + cc_setop(5, "Uncharge"); + } + $owned_total = calc($owned_total + 1); + $cat = db_getfield($row, toolbelt_slots:category, 0); + if ($cat = 0) { + $general = calc($general + 1); + } else if ($cat = 1) { + $farming = calc($farming + 1); + } else if ($cat = 2) { + $fishing = calc($fishing + 1); + } else if ($cat = 4) { + $misc = calc($misc + 1); + } else { + $crafting = calc($crafting + 1); + } + } + } + if ($locked = true) { + if (cc_find($content, $lock) = true) { + $dbg_lock_ok = calc($dbg_lock_ok + 1); + cc_sethide(false); + cc_setposition(calc($slot_x + 6), calc($slot_y + 2), ^setpos_abs_left, ^setpos_abs_top); + cc_setgraphic("magic_spell_padlocks,0"); + cc_setsize(30, 30, ^setsize_abs, ^setsize_abs); + } + } else if (cc_find($content, $lock) = true) { + cc_sethide(true); + } + if ($slot = 27) { + $owned_s = "n"; + $locked_s = "n"; + $ghost_s = "?"; + $obj_s = "?"; + if ($owned = true) { + $owned_s = "y"; + } + if ($locked = true) { + $locked_s = "y"; + } + if ($ghost ! null) { + $ghost_s = oc_name($ghost); + } + if ($obj ! null) { + $obj_s = oc_name($obj); + } + mes("TB bone: stored= owned=<$owned_s> locked=<$locked_s> ghost=<$ghost_s> obj=<$obj_s>"); + } + } else if ($slot = 27) { + mes("TB bone: no obj/ghost (db miss or empty allowed_items)"); + } + $slot = calc($slot + 1); +} +mes("TB draw done: rows= ghosts= items= locks= owned="); +def_int $ci = 0; +def_string $name = ""; +def_int $max = 0; +def_int $have = 0; +while ($ci < 5) { + if (cc_find($content, calc(100 + $ci)) = true) { + $name = enum(int, string, toolbelt_categories, $ci); + $max = ~toolbelt_count_category($ci); + if ($ci = 0) { + $have = $general; + } else if ($ci = 1) { + $have = $farming; + } else if ($ci = 2) { + $have = $fishing; + } else if ($ci = 3) { + $have = $crafting; + } else { + $have = $misc; + } + cc_settext("<$name> - /"); + } + $ci = calc($ci + 1); +} +if (cc_find($border, 1) = true) { + cc_settext("Toolbelt - /28"); +} diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_stored_for_row].cs2 b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_stored_for_row].cs2 new file mode 100644 index 000000000..d7e58ce7a --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/script/[proc,toolbelt_stored_for_row].cs2 @@ -0,0 +1,32 @@ +[proc,toolbelt_stored_for_row](dbrow $row)(int) +switch_dbrow ($row) { + case toolbelt_pickaxe : return(%toolbelt_slot_0); + case toolbelt_axe : return(%toolbelt_slot_1); + case toolbelt_hammer : return(%toolbelt_slot_2); + case toolbelt_chisel : return(%toolbelt_slot_3); + case toolbelt_knife : return(%toolbelt_slot_4); + case toolbelt_tinderbox : return(%toolbelt_slot_5); + case toolbelt_pestle_and_mortar : return(%toolbelt_slot_6); + case toolbelt_spade : return(%toolbelt_slot_7); + case toolbelt_rake : return(%toolbelt_slot_8); + case toolbelt_dibber : return(%toolbelt_slot_9); + case toolbelt_secateurs : return(%toolbelt_slot_10); + case toolbelt_gardening_trowel : return(%toolbelt_slot_11); + case toolbelt_watering_can : return(%toolbelt_slot_12); + case toolbelt_shears : return(%toolbelt_slot_13); + case toolbelt_needle : return(%toolbelt_slot_14); + case toolbelt_glassblowing_pipe : return(%toolbelt_slot_15); + case toolbelt_small_net : return(%toolbelt_slot_16); + case toolbelt_big_net : return(%toolbelt_slot_17); + case toolbelt_fishing_rod : return(%toolbelt_slot_18); + case toolbelt_fly_fishing_rod : return(%toolbelt_slot_19); + case toolbelt_harpoon : return(%toolbelt_slot_20); + case toolbelt_lobster_pot : return(%toolbelt_slot_21); + case toolbelt_amulet_mould : return(%toolbelt_slot_22); + case toolbelt_necklace_mould : return(%toolbelt_slot_23); + case toolbelt_ring_mould : return(%toolbelt_slot_24); + case toolbelt_bracelet_mould : return(%toolbelt_slot_25); + case toolbelt_tiara_mould : return(%toolbelt_slot_26); + case toolbelt_bonecrusher : return(%toolbelt_slot_27); + case default : return(0); +} diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/symbols/clientscript.sym b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/clientscript.sym new file mode 100644 index 000000000..de368da80 --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/clientscript.sym @@ -0,0 +1,6 @@ +45400 [clientscript,toolbelt_init] +45401 [proc,toolbelt_build] +45402 [proc,toolbelt_stored_for_row] +45403 [proc,toolbelt_draw] +45404 [proc,toolbelt_count_category] +45405 [clientscript,toolbelt_filter] diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/symbols/enum.sym b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/enum.sym new file mode 100644 index 000000000..02e96fb8a --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/enum.sym @@ -0,0 +1 @@ +65541 toolbelt_categories diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/symbols/graphic.sym b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/graphic.sym new file mode 100644 index 000000000..f9c94e9bb --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/graphic.sym @@ -0,0 +1 @@ +740 emotes_locked,0 diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/symbols/sprite.sym b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/sprite.sym new file mode 100644 index 000000000..685aa29ea --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/sprite.sym @@ -0,0 +1,4 @@ +21536 square_button +21537 square_button_hover +21538 square_button_clicked +21539 square_button_disabled diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/symbols/varbit.sym b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/varbit.sym new file mode 100644 index 000000000..6577e2776 --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/varbit.sym @@ -0,0 +1,28 @@ +65525 toolbelt_slot_0 int +65524 toolbelt_slot_1 int +65523 toolbelt_slot_2 int +65522 toolbelt_slot_3 int +65521 toolbelt_slot_4 int +65520 toolbelt_slot_5 int +65458 toolbelt_slot_6 int +65456 toolbelt_slot_7 int +65455 toolbelt_slot_8 int +65454 toolbelt_slot_9 int +65453 toolbelt_slot_10 int +65452 toolbelt_slot_11 int +65451 toolbelt_slot_12 int +65450 toolbelt_slot_13 int +65449 toolbelt_slot_14 int +65448 toolbelt_slot_15 int +65447 toolbelt_slot_16 int +65446 toolbelt_slot_17 int +65445 toolbelt_slot_18 int +65444 toolbelt_slot_19 int +65443 toolbelt_slot_20 int +65442 toolbelt_slot_21 int +65441 toolbelt_slot_22 int +65440 toolbelt_slot_23 int +65439 toolbelt_slot_24 int +65438 toolbelt_slot_25 int +65437 toolbelt_slot_26 int +65436 toolbelt_slot_27 int diff --git a/content/other/toolbelt/src/main/resources/pack/cs2/symbols/varp.sym b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/varp.sym new file mode 100644 index 000000000..fc4c563f8 --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/cs2/symbols/varp.sym @@ -0,0 +1,29 @@ +65489 toolbelt_unlocks int +65488 toolbelt_slot_0 int +65487 toolbelt_slot_1 int +65486 toolbelt_slot_2 int +65480 toolbelt_slot_3 int +65479 toolbelt_slot_4 int +65478 toolbelt_slot_5 int +65477 toolbelt_slot_6 int +65476 toolbelt_slot_7 int +65475 toolbelt_slot_8 int +65474 toolbelt_slot_9 int +65473 toolbelt_slot_10 int +65472 toolbelt_slot_11 int +65471 toolbelt_slot_12 int +65470 toolbelt_slot_13 int +65469 toolbelt_slot_14 int +65468 toolbelt_slot_15 int +65467 toolbelt_slot_16 int +65466 toolbelt_slot_17 int +65465 toolbelt_slot_18 int +65464 toolbelt_slot_19 int +65463 toolbelt_slot_20 int +65462 toolbelt_slot_21 int +65461 toolbelt_slot_22 int +65460 toolbelt_slot_23 int +65459 toolbelt_slot_24 int +65458 toolbelt_slot_25 int +65457 toolbelt_slot_26 int +65456 toolbelt_slot_27 int diff --git a/content/other/toolbelt/src/main/resources/pack/scripts/toolbelt-items.rp b/content/other/toolbelt/src/main/resources/pack/scripts/toolbelt-items.rp new file mode 100644 index 000000000..89175ca82 --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/scripts/toolbelt-items.rp @@ -0,0 +1,18 @@ +[after_db.toolbelt_items] +for obj in db.toolbelt_slots.allowed_items { + val ops = oc.iops(obj) + + var anchor = ops.indexOfFirst { it == "Wear" || it == "Wield" } + if (anchor == -1) { + anchor = ops.indexOfFirst { it == "Use" } + } + + var slot = ops.indexOfFirstBlank(anchor + 1) + if (slot == -1) { + slot = ops.indexOfFirstBlank() + } + + if (slot != -1) { + oc.addiop(obj, "Add-to-toolbelt", slot) + } +} diff --git a/content/other/toolbelt/src/main/resources/pack/sprites/manifest.toml b/content/other/toolbelt/src/main/resources/pack/sprites/manifest.toml new file mode 100644 index 000000000..891a24dbe --- /dev/null +++ b/content/other/toolbelt/src/main/resources/pack/sprites/manifest.toml @@ -0,0 +1,5 @@ +tool_belt = "sprites.tool_belt" +square_button = "sprites.square_button" +square_button_hover = "sprites.square_button_hover" +square_button_clicked = "sprites.square_button_clicked" +square_button_disabled = "sprites.square_button_disabled" diff --git a/content/other/toolbelt/src/main/resources/pack/sprites/square_button.png b/content/other/toolbelt/src/main/resources/pack/sprites/square_button.png new file mode 100644 index 000000000..f06e17d90 Binary files /dev/null and b/content/other/toolbelt/src/main/resources/pack/sprites/square_button.png differ diff --git a/content/other/toolbelt/src/main/resources/pack/sprites/square_button_clicked.png b/content/other/toolbelt/src/main/resources/pack/sprites/square_button_clicked.png new file mode 100644 index 000000000..1c0e01946 Binary files /dev/null and b/content/other/toolbelt/src/main/resources/pack/sprites/square_button_clicked.png differ diff --git a/content/other/toolbelt/src/main/resources/pack/sprites/square_button_disabled.png b/content/other/toolbelt/src/main/resources/pack/sprites/square_button_disabled.png new file mode 100644 index 000000000..077560fc5 Binary files /dev/null and b/content/other/toolbelt/src/main/resources/pack/sprites/square_button_disabled.png differ diff --git a/content/other/toolbelt/src/main/resources/pack/sprites/square_button_hover.png b/content/other/toolbelt/src/main/resources/pack/sprites/square_button_hover.png new file mode 100644 index 000000000..70972a4aa Binary files /dev/null and b/content/other/toolbelt/src/main/resources/pack/sprites/square_button_hover.png differ diff --git a/content/other/toolbelt/src/main/resources/pack/sprites/tool_belt.png b/content/other/toolbelt/src/main/resources/pack/sprites/tool_belt.png new file mode 100644 index 000000000..b708e3c10 Binary files /dev/null and b/content/other/toolbelt/src/main/resources/pack/sprites/tool_belt.png differ diff --git a/content/other/toolbelt/src/main/resources/toolbelt.toml b/content/other/toolbelt/src/main/resources/toolbelt.toml new file mode 100644 index 000000000..467cf376f --- /dev/null +++ b/content/other/toolbelt/src/main/resources/toolbelt.toml @@ -0,0 +1,2 @@ +is_enabled = false +allow_higher_tier_unlock = true diff --git a/content/skills/mining/build.gradle.kts b/content/skills/mining/build.gradle.kts index 68f2d54e8..343b567f0 100644 --- a/content/skills/mining/build.gradle.kts +++ b/content/skills/mining/build.gradle.kts @@ -9,4 +9,5 @@ dependencies { implementation(projects.api.dropTablePlugin) implementation(projects.content.quest) implementation(projects.content.skills.utils) + implementation(projects.content.other.toolbelt) } diff --git a/content/skills/mining/src/main/kotlin/org/rsmod/content/skills/mining/scripts/Mining.kt b/content/skills/mining/src/main/kotlin/org/rsmod/content/skills/mining/scripts/Mining.kt index fed50dcc0..592155594 100644 --- a/content/skills/mining/src/main/kotlin/org/rsmod/content/skills/mining/scripts/Mining.kt +++ b/content/skills/mining/src/main/kotlin/org/rsmod/content/skills/mining/scripts/Mining.kt @@ -29,6 +29,7 @@ import org.rsmod.api.script.onOpContentU import org.rsmod.api.stats.levelmod.InvisibleLevels import org.rsmod.api.stats.xpmod.XpModifiers import org.rsmod.api.table.mining.MiningRocksRow +import org.rsmod.content.other.toolbelt.Toolbelt import org.rsmod.content.quest.manager.QuestRequirements import org.rsmod.content.skills.mining.MiningEquipment.miningGloveExtras import org.rsmod.content.skills.mining.MiningEquipment.wearingChargedGlory @@ -423,12 +424,17 @@ constructor( fun findPickaxe(player: Player): InvObj? { val worn = player.wornPickaxe() val carried = player.carriedPickaxes() + val toolbelt = + Toolbelt.resolvePickaxe(player)?.takeIf { getInvObj(it).isUsablePickaxe(player) } val candidates = buildList { if (worn != null) { add(worn) } addAll(carried) + if (toolbelt != null) { + add(toolbelt) + } } return candidates.maxWithOrNull(pickaxeComparator) } diff --git a/content/skills/prayer/build.gradle.kts b/content/skills/prayer/build.gradle.kts index 24a14af58..779f3be13 100644 --- a/content/skills/prayer/build.gradle.kts +++ b/content/skills/prayer/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.api.objCharges) implementation(projects.api.pluginCommons) implementation(projects.api.registry) + implementation(projects.content.other.toolbelt) implementation(projects.content.skills.utils) implementation(projects.engine.events) implementation(projects.engine.utilsBits) diff --git a/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/items/bonecrusher/BonecrusherNpcDropHook.kt b/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/items/bonecrusher/BonecrusherNpcDropHook.kt index 061f5b002..34111cdf3 100644 --- a/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/items/bonecrusher/BonecrusherNpcDropHook.kt +++ b/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/items/bonecrusher/BonecrusherNpcDropHook.kt @@ -6,16 +6,17 @@ import dev.openrune.util.Wearpos import jakarta.inject.Inject import jakarta.inject.Singleton import org.rsmod.api.area.checker.AreaChecker +import org.rsmod.api.death.NpcDeathDropContext +import org.rsmod.api.death.NpcDeathDropHook import org.rsmod.api.player.events.prayer.PrayerSkillAction import org.rsmod.api.player.events.skilling.SkillingActionCompleteEvent import org.rsmod.api.player.events.skilling.SkillingActionContext -import org.rsmod.api.player.vars.boolVarBit -import org.rsmod.api.player.vars.intVarBit -import org.rsmod.api.death.NpcDeathDropContext -import org.rsmod.api.death.NpcDeathDropHook import org.rsmod.api.player.stat.statAdvance import org.rsmod.api.player.stat.statBoost +import org.rsmod.api.player.vars.boolVarBit +import org.rsmod.api.player.vars.intVarBit import org.rsmod.api.table.prayer.SkillPrayerRow +import org.rsmod.content.other.toolbelt.Toolbelt import org.rsmod.events.EventBus import org.rsmod.game.entity.Player import org.rsmod.game.inv.InvObj @@ -55,12 +56,18 @@ constructor( val prayerXp = row.exp.toDouble() / 2 player.statAdvance("stat.prayer", prayerXp) - eventBus.publish(SkillingActionCompleteEvent(player = player, context = - SkillingActionContext.Prayer(PrayerSkillAction.BonecrusherCrushComplete( - boneItemInternal = internal, - experienceGranted = prayerXp, - catacombsBonePrayerRestore = row.prayerRestore - ))), + eventBus.publish( + SkillingActionCompleteEvent( + player = player, + context = + SkillingActionContext.Prayer( + PrayerSkillAction.BonecrusherCrushComplete( + boneItemInternal = internal, + experienceGranted = prayerXp, + catacombsBonePrayerRestore = row.prayerRestore, + ), + ), + ), ) applyPrayerRestoreFromCrush(player, row, source) @@ -71,7 +78,7 @@ constructor( private fun applyPrayerRestoreFromCrush( player: Player, row: SkillPrayerRow, - source: CrusherSource + source: CrusherSource, ) { val restore = row.prayerRestore if (restore <= 0) { @@ -91,10 +98,20 @@ constructor( } } - private fun Player.isBonecrusherNecklacePrayerSource(source: CrusherSource): Boolean = - source.inventory === worn && worn[source.slot]?.isType("obj.bonecrusher_necklace") == true + private fun Player.isBonecrusherNecklacePrayerSource(source: CrusherSource): Boolean { + if (source.fromToolbelt) { + return false + } + if (source.inventory !== worn) { + return false + } + return worn[source.slot]?.isType("obj.bonecrusher_necklace") == true + } private fun Player.isDragonboneNecklaceInvCrushPrayer(source: CrusherSource): Boolean { + if (source.fromToolbelt || source.inventory == null) { + return false + } if (source.inventory !== inv) { return false } @@ -118,6 +135,10 @@ constructor( return CrusherSource(inv, slot) } } + + if (Toolbelt.isEnabled() && Toolbelt.provides(this, "obj.bonecrusher") && bonecrusherCharges > 0) { + return CrusherSource(fromToolbelt = true) + } return null } @@ -129,7 +150,11 @@ constructor( return true } - private data class CrusherSource(val inventory: Inventory, val slot: Int) + private data class CrusherSource( + val inventory: Inventory? = null, + val slot: Int = -1, + val fromToolbelt: Boolean = false, + ) } private var Player.bonecrusherCharges by intVarBit("varbit.charges_bonecrusher_quantity") diff --git a/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/npcs/EctoGhostDiscipleDialogue.kt b/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/npcs/EctoGhostDiscipleDialogue.kt index 3a3c5e9ae..d69eaf432 100644 --- a/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/npcs/EctoGhostDiscipleDialogue.kt +++ b/content/skills/prayer/src/main/kotlin/org/rsmod/content/skills/prayer/npcs/EctoGhostDiscipleDialogue.kt @@ -5,6 +5,7 @@ import dev.openrune.rscm.RSCMType import org.rsmod.api.player.dialogue.Dialogue import org.rsmod.api.player.protect.ProtectedAccess import org.rsmod.api.script.onOpNpc1 +import org.rsmod.content.other.toolbelt.ToolbeltBonecrusherUnlock import org.rsmod.content.skills.prayer.ecto.ectoTokens import org.rsmod.game.entity.Npc import org.rsmod.plugin.scripts.PluginScript @@ -29,6 +30,24 @@ class EctoGhostDiscipleDialogue : PluginScript() { worn.any { ITEM_GHOSTSPEAK_AMULET.contains(it?.id ?: -1) } private suspend fun Dialogue.mainMenu() { + if (ToolbeltBonecrusherUnlock.canOfferUnlock(access.player)) { + when ( + choice2( + "Can you unlock the bonecrusher slot on my tool belt?", + 1, + "Ask about something else.", + 2, + ) + ) { + 1 -> unlockBonecrusherToolbeltSlot() + 2 -> standardMainMenu() + } + return + } + standardMainMenu() + } + + private suspend fun Dialogue.standardMainMenu() { when (choice5( "What is this strange fountain?", 1, @@ -39,7 +58,7 @@ class EctoGhostDiscipleDialogue : PluginScript() { "How do I receive Ectotokens?", 4, "Can I have the tokens I have earned?", - 5 + 5, )) { 1 -> explainFountain() 2 -> explainEctoplasm() @@ -49,6 +68,70 @@ class EctoGhostDiscipleDialogue : PluginScript() { } } + private suspend fun Dialogue.unlockBonecrusherToolbeltSlot() { + chatPlayer( + quiz, + "Can you unlock the bonecrusher slot on my tool belt?", + ) + chatNpc( + neutral, + "Ah, you carry a bonecrusher. For two thousand Ectotokens I can unlock the bonecrusher slot on your tool belt.", + ) + chatNpc( + neutral, + "That only unlocks the slot, mortal. You must still place the bonecrusher on your tool belt yourself.", + ) + when ( + choice2( + "Yes, unlock the bonecrusher tool belt slot. (${ToolbeltBonecrusherUnlock.ECTO_TOKEN_COST} Ectotokens)", + 1, + "No, not yet.", + 2, + ) + ) { + 1 -> confirmUnlockBonecrusherToolbeltSlot() + 2 -> mainMenu() + } + } + + private suspend fun Dialogue.confirmUnlockBonecrusherToolbeltSlot() { + when (val result = ToolbeltBonecrusherUnlock.tryUnlock(access)) { + ToolbeltBonecrusherUnlock.PayResult.Success -> { + chatNpc( + happy, + "It is done. The bonecrusher slot on your tool belt is unlocked. Add the bonecrusher to your tool belt when you are ready.", + ) + } + ToolbeltBonecrusherUnlock.PayResult.AlreadyUnlocked -> { + chatNpc(neutral, "That tool belt slot is already unlocked, mortal.") + } + is ToolbeltBonecrusherUnlock.PayResult.InsufficientTokens -> { + chatNpc( + neutral, + "You need ${ToolbeltBonecrusherUnlock.ECTO_TOKEN_COST} Ectotokens. You only have ${result.held}.", + ) + } + ToolbeltBonecrusherUnlock.PayResult.NeedsBonecrusher -> { + chatNpc( + neutral, + "You must carry a bonecrusher before I can unlock that tool belt slot.", + ) + } + is ToolbeltBonecrusherUnlock.PayResult.InsufficientPrayer -> { + chatNpc( + neutral, + "You need a Prayer level of ${result.required} before I can unlock that tool belt slot.", + ) + } + ToolbeltBonecrusherUnlock.PayResult.NotEnabled, + ToolbeltBonecrusherUnlock.PayResult.NoSlot, + -> { + chatNpc(neutral, "I cannot unlock that tool belt slot for you right now.") + } + } + mainMenu() + } + private suspend fun Dialogue.explainFountain() { chatPlayer(quiz, "What is this strange fountain?") chatNpc( diff --git a/content/skills/woodcutting/build.gradle.kts b/content/skills/woodcutting/build.gradle.kts index 7eca2dc86..746319809 100644 --- a/content/skills/woodcutting/build.gradle.kts +++ b/content/skills/woodcutting/build.gradle.kts @@ -7,4 +7,5 @@ dependencies { implementation(projects.api.pluginCommons) implementation(projects.content.quest) implementation(projects.content.skills.utils) + implementation(projects.content.other.toolbelt) } diff --git a/content/skills/woodcutting/src/main/kotlin/org/rsmod/content/skills/woodcutting/scripts/Woodcutting.kt b/content/skills/woodcutting/src/main/kotlin/org/rsmod/content/skills/woodcutting/scripts/Woodcutting.kt index 391d92e16..65fd8c363 100644 --- a/content/skills/woodcutting/src/main/kotlin/org/rsmod/content/skills/woodcutting/scripts/Woodcutting.kt +++ b/content/skills/woodcutting/src/main/kotlin/org/rsmod/content/skills/woodcutting/scripts/Woodcutting.kt @@ -32,6 +32,7 @@ import org.rsmod.api.script.onOpContentLoc3 import org.rsmod.api.script.onOpContentU import org.rsmod.api.stats.levelmod.InvisibleLevels import org.rsmod.api.stats.xpmod.XpModifiers +import org.rsmod.content.other.toolbelt.Toolbelt import org.rsmod.content.quest.manager.QuestRequirements import org.rsmod.content.skills.woodcutting.configs.WoodcuttingParams import org.rsmod.game.MapClock @@ -277,12 +278,16 @@ constructor( fun findAxe(player: Player, tree: ObjectServerType): InvObj? { val worn = player.wornAxe() val carried = player.carriedAxes() + val toolbelt = Toolbelt.resolveAxe(player)?.takeIf { getInvObj(it).isUsableAxe(player) } val candidates = buildList { if (worn != null) { add(worn) } addAll(carried) + if (toolbelt != null) { + add(toolbelt) + } } return candidates.maxWithOrNull(axeComparator(tree)) } diff --git a/engine/game/src/main/kotlin/org/rsmod/game/inv/InvVirtualStorage.kt b/engine/game/src/main/kotlin/org/rsmod/game/inv/InvVirtualStorage.kt index 9b1cfb365..5fbc58001 100644 --- a/engine/game/src/main/kotlin/org/rsmod/game/inv/InvVirtualStorage.kt +++ b/engine/game/src/main/kotlin/org/rsmod/game/inv/InvVirtualStorage.kt @@ -2,8 +2,12 @@ package org.rsmod.game.inv import org.rsmod.game.entity.Player -public fun interface InvVirtualStorage { +public interface InvVirtualStorage { public fun additionalCount(player: Player, inventory: Inventory, itemInternal: String): Int + + /** True when virtual storage provides any item belonging to [contentGroup]. */ + public fun providesContentGroup(player: Player, inventory: Inventory, contentGroup: Int): Boolean = + false } public object InvVirtualStorageHolder { diff --git a/engine/game/src/main/kotlin/org/rsmod/game/inv/Inventory.kt b/engine/game/src/main/kotlin/org/rsmod/game/inv/Inventory.kt index 130f91e84..72faa33d9 100644 --- a/engine/game/src/main/kotlin/org/rsmod/game/inv/Inventory.kt +++ b/engine/game/src/main/kotlin/org/rsmod/game/inv/Inventory.kt @@ -94,30 +94,59 @@ public class Inventory(public val type: InventoryServerType, public val objs: Ar public operator fun contains(type: String): Boolean { if (type.startsWith("content.")) { val content = type.asRSCM(RSCMType.CONTENT) - return objs.any { obj -> - obj != null && ServerCacheManager.getItem(obj.id)?.contentGroup == content + if ( + objs.any { obj -> + obj != null && + obj.count > 0 && + ServerCacheManager.getItem(obj.id)?.contentGroup == content + } + ) { + return true } + return virtualProvidesContent(content) } return physicalCount(type) > 0 || virtualCount(type) > 0 } public operator fun contains(type: ItemServerType): Boolean = - objs.any { type.isAssociatedWith(it) } + objs.any { it != null && it.count > 0 && type.isAssociatedWith(it) } || + virtualCount(type.internalName) > 0 public fun count(internal: String): Int = physicalCount(internal) + virtualCount(internal) - /** Slots-only count; ignores [InvVirtualStorage]. */ + public fun contentTotal(content: String): Int { + require(content.startsWith("content.")) { "Expected content.*: $content" } + val contentGroup = content.asRSCM(RSCMType.CONTENT) + var total = 0 + for (obj in objs) { + val stack = obj ?: continue + if (stack.count <= 0) { + continue + } + val type = ServerCacheManager.getItem(stack.id) ?: continue + if (type.contentGroup != contentGroup) { + continue + } + total += count(stack, type) + } + if (total > 0) { + return total + } + return if (virtualProvidesContent(contentGroup)) 1 else 0 + } + + /** Slots-only count; ignores [InvVirtualStorage]. Counts of 0 or less are treated as absent. */ public fun physicalCount(internal: String): Int { val objType = ServerCacheManager.getItem(internal.asRSCM(RSCMType.OBJ)) ?: error("Unable to find item: $internal") - val obj = objs.firstOrNull { it?.id == objType.id } ?: return 0 + val obj = objs.firstOrNull { it?.id == objType.id && (it?.count ?: 0) > 0 } ?: return 0 val singleStack = type.stack == InvStackType.Always || objType.isStackable if (singleStack) { - return obj.count + return obj.count.coerceAtLeast(0) } - return individualCount(obj) + return individualCount(obj).coerceAtLeast(0) } private fun virtualCount(internal: String): Int { @@ -126,6 +155,12 @@ public class Inventory(public val type: InventoryServerType, public val objs: Ar return storage.additionalCount(player, this, internal) } + private fun virtualProvidesContent(contentGroup: Int): Boolean { + val player = owner ?: return false + val storage = InvVirtualStorageHolder.instance ?: return false + return storage.providesContentGroup(player, this, contentGroup) + } + public fun count(obj: InvObj, objType: ItemServerType): Int { val singleStack = type.stack == InvStackType.Always || objType.isStackable if (singleStack) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 491b84f1e..3cd21f1ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ shadow = "8.3.7" spotless = "7.0.4" postgresql-jdbc = "42.7.5" embedded-postgres = "2.2.2" -or2 = "2.4.12" +or2 = "2.4.15" openruneCentral = "2.0.1" or2ServerUtils = "0.8" jodatime = "2.14.0" diff --git a/or-cache/build.gradle.kts b/or-cache/build.gradle.kts index f9b69d3b5..07347da41 100644 --- a/or-cache/build.gradle.kts +++ b/or-cache/build.gradle.kts @@ -13,6 +13,8 @@ dependencies { api(libs.or2.definition) api(libs.or2.filestore) api(libs.or2.filesystem) + api(projects.orCache.packApi) + implementation(projects.content.other.toolbelt.pack) implementation(projects.engine.map) implementation(projects.engine.routefinder) implementation("com.michael-bull.kotlin-inline-logger:kotlin-inline-logger:1.0.6") @@ -23,6 +25,7 @@ dependencies { implementation(libs.jackson.databind) implementation("dev.or2:toml-rsconfig:1.0") implementation(libs.fastutil) + implementation(libs.classgraph) } tasks { @@ -43,6 +46,16 @@ tasks { args = listOf("FRESH_INSTALL") } + register("cleanCs2") { + group = "tools" + description = + "Deletes the generated CS2 directory from user app data (recreated on next buildCache)" + classpath = sourceSets["main"].runtimeClasspath + mainClass.set("dev.openrune.CleanCs2Kt") + workingDir = project.projectDir + dependsOn("classes") + } + register("mergePluginGamevals") { group = "cache" description = @@ -55,3 +68,5 @@ tasks { } } + + diff --git a/or-cache/pack-api/build.gradle.kts b/or-cache/pack-api/build.gradle.kts new file mode 100644 index 000000000..41a1ce969 --- /dev/null +++ b/or-cache/pack-api/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("base-conventions") +} + +dependencies { + api(libs.or2.definition) + api(libs.or2.filestore) + api(libs.or2.tools) +} diff --git a/or-cache/pack-api/src/main/kotlin/dev/openrune/pack/PluginPack.kt b/or-cache/pack-api/src/main/kotlin/dev/openrune/pack/PluginPack.kt new file mode 100644 index 000000000..c6014ff64 --- /dev/null +++ b/or-cache/pack-api/src/main/kotlin/dev/openrune/pack/PluginPack.kt @@ -0,0 +1,40 @@ +package dev.openrune.pack + +import dev.openrune.cache.filestore.definition.InterfaceType +import dev.openrune.cache.tools.tasks.CacheTask +import dev.openrune.definition.dbtables.DBTable +import java.io.File + +abstract class PluginPack { + /** + * When true, the full plugin ([configDirectories], sprites, cs2, scripts, interfaces, [dbTables]) + * is packed even if [isEnabled] is false. + */ + open fun shouldAlwaysPack(): Boolean = false + + open fun isEnabled(projectRoot: File): Boolean = true + + fun shouldPack(projectRoot: File): Boolean = shouldAlwaysPack() || isEnabled(projectRoot) + + /** + * Packed when [shouldPack] is false. Use for varbits, enums, and dbtables the server/client still + * need while feature scripts stay off. + */ + open fun configDirectoriesWhenDisabled(projectRoot: File): List = emptyList() + + open fun dbTablesWhenDisabled(): List = emptyList() + + open fun configDirectories(projectRoot: File): List = emptyList() + + open fun spriteDirectories(projectRoot: File): List = emptyList() + + open fun packScriptDirectories(projectRoot: File): List = emptyList() + + open fun cs2Directories(projectRoot: File): List = emptyList() + + open fun dbTables(): List = emptyList() + + open fun interfaces(): List = emptyList() + + open fun extraTasks(): List = emptyList() +} diff --git a/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt b/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt index 3c8f8704a..d5f2cc1b9 100644 --- a/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt +++ b/or-cache/src/main/kotlin/dev/openrune/CacheTools.kt @@ -5,14 +5,18 @@ import dev.openrune.cache.gameval.GameValHandler import dev.openrune.cache.tools.Builder import dev.openrune.cache.tools.CacheEnvironment import dev.openrune.cache.tools.cs2.PackCs2 +import dev.openrune.cache.tools.cs2.SymbolsCustomConflictStrip +import dev.openrune.cache.tools.iftype.PackIfType import dev.openrune.cache.tools.tasks.CacheTask import dev.openrune.cache.tools.tasks.TaskType import dev.openrune.cache.tools.tasks.impl.PackDBTables import dev.openrune.cache.tools.tasks.impl.PackModels +import dev.openrune.cache.tools.tasks.impl.PackSprites import dev.openrune.cache.tools.tasks.impl.defs.PackConfig import dev.openrune.codegen.startEnumGeneration import dev.openrune.codegen.startGeneration import dev.openrune.definition.GameValGroupTypes +import dev.openrune.definition.dbtables.DBTable import dev.openrune.definition.type.DBRowType import dev.openrune.definition.type.DBTableType import dev.openrune.definition.type.EnumType @@ -23,38 +27,47 @@ import dev.openrune.gamevals.GamevalDumper import dev.openrune.impl.GameframeTable import dev.openrune.impl.Music import dev.openrune.map.packing.MapPackers +import dev.openrune.pack.PluginPack +import dev.openrune.pack.PluginPackLoader +import dev.openrune.packscript.PackScriptPhase +import dev.openrune.packscript.PackScripts import dev.openrune.tables.DidYouKnow import dev.openrune.tables.InstanceSettingsTable import dev.openrune.tables.PickableObjects import dev.openrune.tables.SettingConfigs import dev.openrune.tables.ShopCurrencyTable import dev.openrune.tables.StatComponents +import dev.openrune.tables.consumables.food.FoodTable +import dev.openrune.tables.consumables.potion.PotionEffectTable +import dev.openrune.tables.consumables.potion.PotionTable import dev.openrune.tables.skills.Cooking import dev.openrune.tables.skills.Firemaking import dev.openrune.tables.skills.Herblore import dev.openrune.tables.skills.Mining -import dev.openrune.tables.skills.Slayer import dev.openrune.tables.skills.Runecrafting import dev.openrune.tables.skills.ShootingStars +import dev.openrune.tables.skills.Slayer import dev.openrune.tables.skills.Smithing import dev.openrune.tables.skills.prayer.EctofuntusBonemeal import dev.openrune.tables.skills.prayer.PrayerBlessedBone import dev.openrune.tables.skills.prayer.PrayerTable import dev.openrune.tools.MinifyServerCache import dev.openrune.tools.PackServerConfig -import dev.openrune.tables.consumables.food.FoodTable -import dev.openrune.tables.consumables.potion.PotionEffectTable -import dev.openrune.tables.consumables.potion.PotionTable import java.io.File import kotlin.system.exitProcess -fun getCacheLocation() = File("../.data/", "cache/LIVE").path +private val logger = InlineLogger() -fun getServerCacheLocation() = File("../.data/", "cache/SERVER").path +private val dataRoot = File("../.data") +private val rawCache = File(dataRoot, "raw-cache") -val revision : Triple = readRevision() +fun getCacheLocation() = File(dataRoot, "cache/LIVE").path -private val logger = InlineLogger() +fun getServerCacheLocation() = File(dataRoot, "cache/SERVER").path + +fun getCs2Location(): File = DirectoryConstants.CS2_PATH.toFile() + +val revision: Triple = readRevision() val VARBIT = CacheVarLiteral.registerExternal(254, ']', name = "VARBIT") @@ -65,7 +78,6 @@ fun main(args: Array) { } CacheVarLiteral.registerExternal(253, '[', name = "PROJANIM") - downloadRev(TaskType.valueOf(args.first().uppercase())) } @@ -110,69 +122,52 @@ fun tablesToPack() = listOf( ) fun downloadRev(type: TaskType) { - logger.info { "Using Revision: $revision" } - when (type) { - TaskType.FRESH_INSTALL -> { - - val builder = - Builder( - type = TaskType.FRESH_INSTALL, - cacheLocation = File(getCacheLocation()), - serverCacheLocation = File(getServerCacheLocation()), - ) - builder.revision(revision.first) - builder.subRevision(revision.second) - builder.removeXteas(false) - builder.environment(CacheEnvironment.valueOf(revision.third)) - - builder.build().initialize() - - File(getServerCacheLocation(), "xteas.json").delete() - - val cache = Cache.load(File(getCacheLocation()).toPath()) - - GamevalDumper.dumpGamevals(cache, revision.first) + if (type == TaskType.FRESH_INSTALL) { + createBuilder(TaskType.FRESH_INSTALL) + .apply { + subRevision(revision.second) + removeXteas(false) + environment(CacheEnvironment.valueOf(revision.third)) + } + .build() + .initialize() - buildCache(TaskType.BUILD) - } - TaskType.SERVER_CACHE_BUILD -> buildCache(TaskType.SERVER_CACHE_BUILD) - TaskType.BUILD -> buildCache(TaskType.BUILD) + File(getServerCacheLocation(), "xteas.json").delete() + GamevalDumper.dumpGamevals(Cache.load(File(getCacheLocation()).toPath()), revision.first) + buildCache(TaskType.BUILD) + return } + + buildCache(type) } fun buildCache(taskType: TaskType) { GameValProvider.load("../", autoAssignIds = true) - val tasks: List = - listOf( - PackModels(File("../.data/raw-cache/models")), - PackConfig(File("../.data/raw-cache/server")), - PackDBTables(tablesToPack()) - ).toMutableList() - - val builder = - Builder( - type = taskType, - cacheLocation = File(getCacheLocation()), - serverCacheLocation = File(getServerCacheLocation()), - ) - builder.revision(revision.first) + val projectRoot = File("..") + val pluginPacks = PluginPackLoader.load() + val activePacks = pluginPacks.filter { it.shouldPack(projectRoot) } + val disabledPacks = pluginPacks.filter { !it.shouldPack(projectRoot) } + syncPluginCs2(projectRoot, pluginPacks, activePacks) + + val allTables = + tablesToPack() + + activePacks.flatMap { it.dbTables() } + + disabledPacks.flatMap { it.dbTablesWhenDisabled() } + + val tasks = buildPackTasks(projectRoot, activePacks, disabledPacks, allTables) + val builder = createBuilder(taskType) builder.extraTasks(*tasks.toTypedArray()).build().initialize() if (taskType == TaskType.BUILD) { - val serverTasks = tasks.filterNot { it is PackCs2 } - + val serverTasks = tasks.filterNot { it is PackCs2 || it is PackIfType || it is PackScripts } builder.type = TaskType.SERVER_CACHE_BUILD - builder .extraTasks( - PackServerConfig( - revision.first, - File("../.data/raw-cache/server") - ), + PackServerConfig(revision.first, File(rawCache, "server")), MapPackers(), *serverTasks.toTypedArray(), ) @@ -181,24 +176,80 @@ fun buildCache(taskType: TaskType) { } if (builder.type == TaskType.SERVER_CACHE_BUILD) { - MinifyServerCache().init(getServerCacheLocation()) - val cache = Cache.load(File(getServerCacheLocation()).toPath()) - GamevalDumper.dumpCols(cache, revision.first) + finalizeServerCache() + } +} - val type = GameValHandler.readGameVal(GameValGroupTypes.TABLETYPES, cache = cache, revision.first) +private fun createBuilder(type: TaskType) = + Builder( + type = type, + cacheLocation = File(getCacheLocation()), + serverCacheLocation = File(getServerCacheLocation()), + ).also { it.revision(revision.first) } + +private fun buildPackTasks( + projectRoot: File, + activePacks: List, + disabledPacks: List, + allTables: List, +): MutableList { + val pluginConfigDirs = + ( + activePacks.flatMap { it.configDirectories(projectRoot) } + + disabledPacks.flatMap { it.configDirectoriesWhenDisabled(projectRoot) } + ).filter { it.isDirectory } + val pluginSpriteDirs = + activePacks.flatMap { it.spriteDirectories(projectRoot) }.filter { it.isDirectory } + val pluginScriptDirs = + activePacks.flatMap { it.packScriptDirectories(projectRoot) }.filter { it.isDirectory } + val pluginInterfaces = activePacks.flatMap { it.interfaces() } + + val tasks = mutableListOf( + PackModels(File(rawCache, "models")), + PackConfig(File(rawCache, "server")), + ) + pluginConfigDirs.forEach { tasks += PackConfig(it) } + + val legacySpritesDir = File(rawCache, "sprites") + if (legacySpritesDir.isDirectory) { + tasks += PackSprites(legacySpritesDir) + } + pluginSpriteDirs.forEach { tasks += PackSprites(it) } + tasks += activePacks.flatMap { it.extraTasks() } - val rows: MutableMap = mutableMapOf() - OsrsCacheProvider.DBRowDecoder().load(cache, rows) + if (pluginInterfaces.isNotEmpty()) { + tasks += PackIfType(pluginInterfaces) + } + tasks += PackCs2(getCs2Location()) + tasks += PackDBTables(allTables) - val enums: MutableMap = mutableMapOf() - OsrsCacheProvider.EnumDecoder().load(cache, enums) + if (pluginScriptDirs.isNotEmpty()) { + tasks += PackScripts(pluginScriptDirs, PackScriptPhase.AFTER_DB, allTables) + } + return tasks +} - val dbtables: MutableMap = mutableMapOf() - OsrsCacheProvider.DBTableDecoder().load(cache, dbtables) +private fun finalizeServerCache() { + MinifyServerCache().init(getServerCacheLocation()) - startGeneration(type, rows, enums, dbtables) - startEnumGeneration(enums) - } + val cache = Cache.load(File(getServerCacheLocation()).toPath()) + GamevalDumper.dumpCols(cache, revision.first) + GamevalDumper.dumpComponents(cache, revision.first) + + val type = + GameValHandler.readGameVal(GameValGroupTypes.TABLETYPES, cache = cache, revision.first) + + val rows = mutableMapOf() + OsrsCacheProvider.DBRowDecoder().load(cache, rows) + + val enums = mutableMapOf() + OsrsCacheProvider.EnumDecoder().load(cache, enums) + + val dbtables = mutableMapOf() + OsrsCacheProvider.DBTableDecoder().load(cache, dbtables) + + startGeneration(type, rows, enums, dbtables) + startEnumGeneration(enums) } fun readRevision(): Triple { @@ -206,26 +257,140 @@ fun readRevision(): Triple { listOf("../game.yml", "../game.example.yml").map(::File).firstOrNull { it.exists() } ?: error("No game.yml or game.example.yml found") - return file.useLines { lines -> - val revisionLine = - lines.firstOrNull { it.trimStart().startsWith("revision:") } - ?: error("No revision line found in ${file.name}") + val lines = file.readLines() + val revisionLine = + lines.firstOrNull { it.trimStart().startsWith("revision:") } + ?: error("No revision line found in ${file.name}") + + val revisionStr = revisionLine.substringAfter("revision:").trim() + val match = + Regex("""^(\d+)(?:\.(\d+))?$""").matchEntire(revisionStr) + ?: error("Invalid revision format: '$revisionStr'") - val revisionStr = revisionLine.substringAfter("revision:").trim() - val match = - Regex("""^(\d+)(?:\.(\d+))?$""").matchEntire(revisionStr) - ?: error("Invalid revision format: '$revisionStr'") + val major = match.groupValues[1].toInt() + val minor = match.groupValues.getOrNull(2)?.toIntOrNull() ?: -1 - val major = match.groupValues[1].toInt() - val minor = match.groupValues.getOrNull(2)?.toIntOrNull() ?: -1 + val envLine = lines.firstOrNull { it.trimStart().startsWith("environment:") } + val environment = + envLine?.substringAfter("environment:")?.trim()?.removeSurrounding("\"")?.ifBlank { "live" } + ?: "live" + + return Triple(major, minor, environment.uppercase()) +} - val envLine = file.readLines().firstOrNull { it.trimStart().startsWith("environment:") } +private fun syncPluginCs2( + projectRoot: File, + allPacks: List, + activePacks: List, +) { + val cs2Root = getCs2Location().also { it.mkdirs() } + val customRoot = File(cs2Root, "custom").also { it.mkdirs() } + val symbolsCustom = File(cs2Root, "symbols_custom").also { it.mkdirs() } - val environment = - envLine?.substringAfter("environment:")?.trim()?.removeSurrounding("\"")?.ifBlank { - "live" - } ?: "live" + fun packName(pack: PluginPack): String = + pack::class.java.name.substringAfterLast('.').removeSuffix("PluginPack").lowercase() - Triple(major, minor, environment.uppercase()) + val activeNames = activePacks.map(::packName).toSet() + val allNames = allPacks.map(::packName).toSet() + + customRoot.listFiles()?.forEach { child -> + if (child.isDirectory && child.name.lowercase() in allNames && child.name.lowercase() !in activeNames) { + child.deleteRecursively() + } } + File(customRoot, "_plugins").takeIf { it.exists() }?.deleteRecursively() + File(symbolsCustom, "_plugins").takeIf { it.exists() }?.deleteRecursively() + + for (pack in allPacks) { + for (dir in pack.cs2Directories(projectRoot).filter { it.isDirectory }) { + val symbols = File(dir, "symbols") + if (!symbols.isDirectory) continue + symbols.listFiles()?.filter { it.isFile && it.extension.equals("sym", true) } + ?.forEach { sym -> + stripSymbolLines(File(symbolsCustom, sym.name), readSymbolLines(sym)) + } + } + } + + for (pack in activePacks) { + val name = packName(pack) + val scriptDest = + File(customRoot, name).also { + it.deleteRecursively() + it.mkdirs() + } + + for (dir in pack.cs2Directories(projectRoot).filter { it.isDirectory }) { + val scripts = File(dir, "script") + if (scripts.isDirectory) { + scripts.walkTopDown().filter { it.isFile && it.extension.equals("cs2", true) } + .forEach { src -> + src.copyTo(File(scriptDest, src.name), overwrite = true) + } + } + dir.listFiles()?.filter { it.isFile && it.extension.equals("cs2", true) } + ?.forEach { src -> + src.copyTo(File(scriptDest, src.name), overwrite = true) + } + + val symbols = File(dir, "symbols") + if (symbols.isDirectory) { + symbols.listFiles()?.filter { it.isFile && it.extension.equals("sym", true) } + ?.forEach { sym -> + mergeSymbolLines(File(symbolsCustom, sym.name), readSymbolLines(sym)) + } + } + } + } + + SymbolsCustomConflictStrip.strip(cs2Root) +} + +private val SYMBOL_LINE = Regex("""^\s*(\S+)\s+(.+?)\s*$""") + +private fun readSymbolLines(file: File): List> = + file.readLines().mapNotNull { line -> + val trimmed = line.trim() + if (trimmed.isEmpty() || trimmed.startsWith("#")) return@mapNotNull null + val match = SYMBOL_LINE.matchEntire(trimmed) ?: return@mapNotNull null + match.groupValues[1] to match.groupValues[2].trim() + } + +private fun symbolName(rest: String): String = + rest.substringBefore('\t').substringBefore(' ').trim() + +private fun stripSymbolLines(target: File, owned: List>) { + if (!target.exists() || owned.isEmpty()) return + val ownedIds = owned.map { it.first }.toSet() + val ownedNames = owned.map { symbolName(it.second) }.toSet() + val kept = + target.readLines().filter { line -> + val trimmed = line.trim() + if (trimmed.isEmpty()) return@filter false + val match = SYMBOL_LINE.matchEntire(trimmed) ?: return@filter true + val id = match.groupValues[1] + val name = symbolName(match.groupValues[2]) + id !in ownedIds && name !in ownedNames + } + if (kept.isEmpty()) { + target.delete() + } else { + target.writeText(kept.joinToString("\n", postfix = "\n")) + } +} + +private fun mergeSymbolLines(target: File, lines: List>) { + if (lines.isEmpty()) return + stripSymbolLines(target, lines) + val existing = if (target.exists()) target.readText().trimEnd() else "" + val body = lines.joinToString("\n") { (id, rest) -> "$id\t$rest" } + target.parentFile?.mkdirs() + target.writeText( + buildString { + if (existing.isNotBlank()) { + appendLine(existing) + } + appendLine(body) + }, + ) } diff --git a/or-cache/src/main/kotlin/dev/openrune/CleanCs2.kt b/or-cache/src/main/kotlin/dev/openrune/CleanCs2.kt new file mode 100644 index 000000000..0beec8000 --- /dev/null +++ b/or-cache/src/main/kotlin/dev/openrune/CleanCs2.kt @@ -0,0 +1,13 @@ +package dev.openrune + +fun main() { + val dir = DirectoryConstants.CS2_PATH.toFile() + if (!dir.exists()) { + println("No CS2 directory at ${dir.absolutePath}") + return + } + if (!dir.deleteRecursively()) { + error("Failed to delete ${dir.absolutePath}") + } + println("Deleted CS2 directory: ${dir.absolutePath}") +} diff --git a/or-cache/src/main/kotlin/dev/openrune/DirectoryConstants.kt b/or-cache/src/main/kotlin/dev/openrune/DirectoryConstants.kt index 077946a31..965494843 100644 --- a/or-cache/src/main/kotlin/dev/openrune/DirectoryConstants.kt +++ b/or-cache/src/main/kotlin/dev/openrune/DirectoryConstants.kt @@ -6,4 +6,53 @@ import java.nio.file.Paths object DirectoryConstants { val DATA_PATH: Path = Paths.get(".data") val CACHE_PATH: Path = DATA_PATH.resolve("cache") + + val CS2_PATH: Path = userAppDataDir().resolve(projectAppName()).resolve("cs2") + + fun projectAppName(): String { + val file = + listOf("game.yml", "../game.yml", "game.example.yml", "../game.example.yml") + .map { Paths.get(it).toFile() } + .firstOrNull { it.exists() } + + val raw = + file + ?.readLines() + ?.firstOrNull { it.trimStart().startsWith("name:") } + ?.substringAfter("name:") + ?.trim() + ?.removeSurrounding("\"") + ?.removeSurrounding("'") + ?.trim() + .orEmpty() + + val sanitized = + raw.replace(Regex("""[<>:"/\\|?*\u0000-\u001F]"""), "_") + .trim() + .trimStart('.') + .trimEnd('.') + + return sanitized.ifBlank { "OpenRune" } + } + + fun userAppDataDir(): Path { + val os = System.getProperty("os.name").lowercase() + val home = Paths.get(System.getProperty("user.home")) + return when { + os.contains("win") -> { + val local = System.getenv("LOCALAPPDATA") + val roaming = System.getenv("APPDATA") + when { + !local.isNullOrBlank() -> Paths.get(local) + !roaming.isNullOrBlank() -> Paths.get(roaming) + else -> home.resolve("AppData").resolve("Local") + } + } + os.contains("mac") -> home.resolve("Library").resolve("Application Support") + else -> { + val xdg = System.getenv("XDG_DATA_HOME") + if (!xdg.isNullOrBlank()) Paths.get(xdg) else home.resolve(".local").resolve("share") + } + } + } } diff --git a/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/ItemServerCodec.kt b/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/ItemServerCodec.kt index e6be8f061..88a8bf6eb 100644 --- a/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/ItemServerCodec.kt +++ b/or-cache/src/main/kotlin/dev/openrune/codec/osrs/impl/ItemServerCodec.kt @@ -96,7 +96,7 @@ class ItemServerCodec( stockmarket = item.stockMarket category = item.category options = item.options - interfaceOptions = item.interfaceOptions + interfaceOptions = item.interfaceOptions.toMutableList() certlink = item.noteLinkId certtemplate = item.noteTemplateId placeholderLink = item.placeholderLink diff --git a/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValProvider.kt b/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValProvider.kt index 453e29f7d..3303dc49c 100644 --- a/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValProvider.kt +++ b/or-cache/src/main/kotlin/dev/openrune/gamevals/GameValProvider.kt @@ -21,6 +21,7 @@ class GameValProvider : MappingProvider { fun sourceFiles(rootDir: String): Array = arrayOf( Paths.get("${rootDir}.data", "gamevals-binary", "gamevals.dat").toFile(), + Paths.get("${rootDir}.data", "gamevals-binary", "gamevals_components.dat").toFile(), Paths.get("${rootDir}.data", "gamevals-binary", "gamevals_columns.dat").toFile(), Paths.get("${rootDir}content").toFile(), Paths.get("${rootDir}api").toFile(), @@ -42,16 +43,17 @@ class GameValProvider : MappingProvider { } override fun load(vararg files: File) { - require(files.size >= 2) { - "Expected at least two files for loading: gamevals.dat and gamevals_columns.dat" + require(files.size >= 3) { + "Expected at least three files for loading: gamevals.dat, gamevals_components.dat and gamevals_columns.dat" } decodeGameValDat(files[0]) decodeGameValDat(files[1]) + decodeGameValDat(files[2]) - val contentDir = files.getOrNull(2)?.takeIf { it.exists() && it.isDirectory } - val apiDir = files.getOrNull(3)?.takeIf { it.exists() && it.isDirectory } - val gamevalsDir = files.getOrNull(4)?.takeIf { it.exists() && it.isDirectory } + val contentDir = files.getOrNull(3)?.takeIf { it.exists() && it.isDirectory } + val apiDir = files.getOrNull(4)?.takeIf { it.exists() && it.isDirectory } + val gamevalsDir = files.getOrNull(5)?.takeIf { it.exists() && it.isDirectory } if (autoAssignIds && (contentDir != null || apiDir != null || gamevalsDir != null)) { GameValAutoAssigner(mappings, maxBaseID).run(contentDir, apiDir, gamevalsDir) @@ -102,16 +104,16 @@ class GameValProvider : MappingProvider { } private fun parseGameValTomlEntry(line: String, source: String): Pair? { - if (line.startsWith("[")) { - return null - } - val equalsIndex = line.indexOf('=') if (equalsIndex <= 0) { return null } - val key = line.substring(0, equalsIndex).trim() + val key = + line.substring(0, equalsIndex) + .trim() + .removeSurrounding("\"") + .removeSurrounding("'") val value = line.substring(equalsIndex + 1).trim().toIntOrNull() if (value == null) { return null @@ -194,6 +196,9 @@ class GameValProvider : MappingProvider { } private fun decodeGameValDat(datFile: File) { + if (!datFile.isFile) { + return + } DataInputStream(FileInputStream(datFile)).use { input -> val tableCount = input.readInt() repeat(tableCount) { diff --git a/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt b/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt index c4b80d339..e46492a1d 100644 --- a/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt +++ b/or-cache/src/main/kotlin/dev/openrune/gamevals/GamevalDumper.kt @@ -40,16 +40,7 @@ object GamevalDumper { GameValGroupTypes.IFTYPES_V2 -> { val interfaces = elements.mapNotNull { it.elementAs() } - - gamevals["interfaces"] = - interfaces.map { "${it.name}=${it.id}" } - - gamevals["components"] = - interfaces.flatMap { iface -> - iface.components.map { comp -> - "${iface.name}:${comp.name}=${comp.packed}" - } - } + gamevals["interfaces"] = interfaces.map { "${it.name}=${it.id}" } } GameValGroupTypes.IFTYPES -> Unit @@ -66,7 +57,24 @@ object GamevalDumper { dumpCols(cache, rev) + dumpComponents(cache, rev) + } + + fun dumpComponents(cache: Cache, rev: Int) { + val elements = GameValHandler.readGameVal(GameValGroupTypes.IFTYPES_V2, cache = cache, rev) + val data = mutableListOf() + + elements.forEach { gameValElement -> + val iftype = gameValElement.elementAs() ?: return@forEach + iftype.components.forEach { comp -> + data.add("${iftype.name}:${comp.name}=${comp.packed}") + } + } + + encodeGameValDat("../.data/gamevals-binary/gamevals_components.dat", mapOf("component" to data)) + GameValMaxIdManifest.writeFromDats(File(".."), revision = rev) + } fun dumpCols(cache: Cache, rev: Int) { diff --git a/or-cache/src/main/kotlin/dev/openrune/pack/PluginPackLoader.kt b/or-cache/src/main/kotlin/dev/openrune/pack/PluginPackLoader.kt new file mode 100644 index 000000000..b800c570b --- /dev/null +++ b/or-cache/src/main/kotlin/dev/openrune/pack/PluginPackLoader.kt @@ -0,0 +1,26 @@ +package dev.openrune.pack + +import io.github.classgraph.ClassGraph + +object PluginPackLoader { + private val packages = arrayOf("dev.openrune.pack", "org.rsmod.content") + + fun load(): List { + val packs = mutableListOf() + ClassGraph() + .ignoreClassVisibility() + .enableClassInfo() + .disableNestedJarScanning() + .disableModuleScanning() + .acceptPackages(*packages) + .scan() + .use { result -> + result.getSubclasses(PluginPack::class.java).directOnly().forEach { info -> + val clazz = info.loadClass(PluginPack::class.java) + val ctor = clazz.getConstructor() + packs += ctor.newInstance() + } + } + return packs + } +} diff --git a/or-cache/src/main/kotlin/dev/openrune/packscript/PackLangAst.kt b/or-cache/src/main/kotlin/dev/openrune/packscript/PackLangAst.kt new file mode 100644 index 000000000..9e7b375ad --- /dev/null +++ b/or-cache/src/main/kotlin/dev/openrune/packscript/PackLangAst.kt @@ -0,0 +1,99 @@ +package dev.openrune.packscript + +/** + * RunePack — tiny Kotlin-flavoured pack-time scripting. + * + * ``` + * [after_db.toolbelt_items] + * for obj in db.toolbelt_slots.allowed_items { + * val ops = oc.iops(obj) + * val anchor = ops.indexOfFirst { it == "Wear" || it == "Wield" } + * oc.addiop(obj, "Add-to-toolbelt", slot) + * } + * ``` + */ +data class PackScript( + val phase: PackScriptPhase, + val name: String, + val body: List, + val source: String, +) + +sealed interface Stmt { + data class ForIn( + val binding: String, + val iterable: Expr, + val body: List, + ) : Stmt + + data class While( + val cond: Expr, + val body: List, + ) : Stmt + + data class If( + val cond: Expr, + val thenBody: List, + val elseBody: List, + ) : Stmt + + data class Val( + val name: String, + val value: Expr, + val mutable: Boolean, + ) : Stmt + + data class Assign( + val name: String, + val value: Expr, + ) : Stmt + + data class Call(val path: List, val args: List) : Stmt +} + +sealed interface Expr { + data class StringLit(val value: String) : Expr + + data class IntLit(val value: Int) : Expr + + data class BoolLit(val value: Boolean) : Expr + + data object NullLit : Expr + + data class Ident(val name: String) : Expr + + data class Path(val parts: List) : Expr + + data class Call(val path: List, val args: List) : Expr + + /** `{ it -> expr }` or `{ expr }` (param defaults to `it`). */ + data class Lambda(val param: String, val body: Expr) : Expr + + data class Unary(val op: UnaryOp, val expr: Expr) : Expr + + data class Binary(val op: BinaryOp, val left: Expr, val right: Expr) : Expr + + data class IfExpr(val cond: Expr, val thenExpr: Expr, val elseExpr: Expr) : Expr +} + +/** Runtime closure created when a lambda expression is evaluated. */ +data class LambdaValue( + val param: String, + val body: Expr, + val closure: Map, +) + +enum class UnaryOp { NOT, NEG } + +enum class BinaryOp { + EQ, + NEQ, + LT, + LTE, + GT, + GTE, + AND, + OR, + ADD, + SUB, +} diff --git a/or-cache/src/main/kotlin/dev/openrune/packscript/PackLangParser.kt b/or-cache/src/main/kotlin/dev/openrune/packscript/PackLangParser.kt new file mode 100644 index 000000000..0838ec929 --- /dev/null +++ b/or-cache/src/main/kotlin/dev/openrune/packscript/PackLangParser.kt @@ -0,0 +1,547 @@ +package dev.openrune.packscript + +import java.io.File + +/** + * Parser for RunePack (Kotlin-lite): + * + * ``` + * [after_db.name] + * val x = oc.equipable(obj) + * if (x) { ... } else { ... } + * for obj in db.table.col { oc.addiop(obj, "...") } + * ``` + */ +object PackLangParser { + fun parseFile(file: File): PackScript { + val text = file.readText() + return parse(text, file.name) + } + + fun parse(source: String, sourceName: String = "