From 6d23ec523487abcf64937540c4824e54ee512985 Mon Sep 17 00:00:00 2001 From: Ceikry Date: Wed, 2 Aug 2023 00:39:04 +0000 Subject: [PATCH] Added farming patch safety checks --- .../global/skill/farming/FarmingPatch.kt | 31 +++++++ .../content/global/skill/farming/Patch.kt | 59 ++++++++++---- .../skill/farming/UseWithPatchHandler.kt | 20 +---- .../cache/def/impl/SceneryDefinition.java | 14 +++- Server/src/test/kotlin/TestUtils.kt | 2 + .../content/skill/farming/PatchTests.kt | 81 +++++++++++++++++++ 6 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 Server/src/test/kotlin/content/skill/farming/PatchTests.kt diff --git a/Server/src/main/content/global/skill/farming/FarmingPatch.kt b/Server/src/main/content/global/skill/farming/FarmingPatch.kt index e32d2b43f..f241b338b 100644 --- a/Server/src/main/content/global/skill/farming/FarmingPatch.kt +++ b/Server/src/main/content/global/skill/farming/FarmingPatch.kt @@ -74,6 +74,33 @@ enum class FarmingPatch(val varbit: Int, val type: PatchType) { companion object { @JvmField val patches = FarmingPatch.values().map { it.varbit to it }.toMap() + val patchNodes = ArrayList() + val nodeMap = HashMap() + + init { + patchNodes.addAll(8550..8557) //allotment wrappers + patchNodes.addAll(7847..7853) //flower patch wrappers + patchNodes.addAll(8150..8156) //herb patch wrappers + patchNodes.addAll(8388..8391) // Tree patches + patchNodes.add(19147) //Tree patch + patchNodes.addAll(7962..7965) //fruit trees + patchNodes.addAll(8173..8176) //hops + patchNodes.addAll(7577..7580) //bush + patchNodes.add(23760) //evil turnip + patchNodes.add(7572) //belladonna + patchNodes.add(8337) //mushroom + patchNodes.add(27197) //jade vine + patchNodes.add(7771) //cactus + patchNodes.add(7807) //calquat + patchNodes.addAll(8382..8383)//spirit trees + patchNodes.add(8338) //spirit tree + patchNodes.add(18816) //death plateau wrapper + + for (patch in patchNodes) { + val def = SceneryDefinition.forId(patch) + nodeMap[def.varbitID] = def + } + } @JvmStatic fun forObject(obj: Scenery): FarmingPatch?{ @@ -85,6 +112,10 @@ enum class FarmingPatch(val varbit: Int, val type: PatchType) { val objDef = SceneryDefinition.forId(id) return patches[objDef.varbitID] } + + fun getSceneryDefByVarbit (id: Int) : SceneryDefinition? { + return nodeMap[id] + } } fun getPatchFor(player: Player): Patch{ diff --git a/Server/src/main/content/global/skill/farming/Patch.kt b/Server/src/main/content/global/skill/farming/Patch.kt index 13f082e62..1bbc82e4f 100644 --- a/Server/src/main/content/global/skill/farming/Patch.kt +++ b/Server/src/main/content/global/skill/farming/Patch.kt @@ -98,6 +98,35 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl updateBit() } + fun setVisualState (state: Int) { + val finalState = ensureStateSanity(state) + setVarbit(player, patch.varbit, finalState) + } + + fun ensureStateSanity (state: Int) : Int { + val patchDef = FarmingPatch.getSceneryDefByVarbit(patch.varbit) ?: return state + val currentStateDef = patchDef.getChildObjectAtIndex(state) + if (currentStateDef.name == patchDef.getChildObjectAtIndex(3).name) { //if we're weedy + if (state and 0x40 != 0) { //if this invalid state was caused by water/death + //remove water/death + isDead = false + isWatered = false + log(this::class.java, Log.DEBUG, "Patch for ${player.username} at varbit ${patch.varbit} with plantable ${plantable?.name ?: "none"} was set to watered/dead at stage $currentGrowthStage, which isn't valid.") + return (state and (0x40.inv())) + } + else if (state and 0x80 != 0) { //if this invalid state was caused by disease + //remove disease + isDiseased = false + log(this::class.java, Log.DEBUG, "Patch for ${player.username} at varbit ${patch.varbit} with plantable ${plantable?.name ?: "none"} was set to diseased at stage $currentGrowthStage, which isn't valid.") + return (state and (0x80.inv())) + } + else { + log (this::class.java, Log.ERR, "Patch for ${player.username} at varbit ${patch.varbit} with plantable ${plantable?.name ?: "none"} was set to state $state at growth stage $currentGrowthStage, which isn't valid. We're not sure why this is happening.") + } + } + return state + } + fun isFertilized(): Boolean { return compost != CompostType.NONE } @@ -106,7 +135,7 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl return currentGrowthStage == (plantable?.stages ?: 0) } - private fun updateBit(){ + fun updateBit(){ if(isCheckHealth){ when(patch.type){ PatchType.FRUIT_TREE -> setVarbit(player, patch.varbit, plantable!!.value + plantable!!.stages + 20) @@ -122,11 +151,11 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl if (isDiseased) state = state or 0x80 if (state != getVarbit(player, patch.varbit)) - setVarbit(player, patch.varbit, state) + setVisualState(state) } PatchType.BUSH -> { - if(isDead) setVarbit(player, patch.varbit, getBushDeathValue()) - else if(isDiseased && !isDead) setVarbit(player, patch.varbit, getBushDiseaseValue()) + if(isDead) setVisualState(getBushDeathValue()) + else if(isDiseased && !isDead) setVisualState(getBushDiseaseValue()) } PatchType.TREE -> { var state = getVarbit(player, patch.varbit) @@ -135,25 +164,25 @@ class Patch(val player: Player, val patch: FarmingPatch, var plantable: Plantabl else if (isDiseased) state = state or 0x40 if (state != getVarbit(player, patch.varbit)) - setVarbit(player, patch.varbit, state) + setVisualState(state) } PatchType.FRUIT_TREE -> { - if(isDead) setVarbit(player, patch.varbit, getFruitTreeDeathValue()) - else if(isDiseased && !isDead) setVarbit(player, patch.varbit, getFruitTreeDiseaseValue()) + if(isDead) setVisualState(getFruitTreeDeathValue()) + else if(isDiseased && !isDead) setVisualState(getFruitTreeDiseaseValue()) } PatchType.BELLADONNA -> { - if(isDead) setVarbit(player, patch.varbit, getBelladonnaDeathValue()) - else if(isDiseased && !isDead) setVarbit(player, patch.varbit, getBelladonnaDiseaseValue()) - else setVarbit(player, patch.varbit, (plantable?.value ?: 0) + currentGrowthStage) + if(isDead) setVisualState(getBelladonnaDeathValue()) + else if(isDiseased && !isDead) setVisualState(getBelladonnaDiseaseValue()) + else setVisualState((plantable?.value ?: 0) + currentGrowthStage) } PatchType.CACTUS -> { - if(isDead) setVarbit(player, patch.varbit, getCactusDeathValue()) - else if(isDiseased && !isDead) setVarbit(player, patch.varbit, getCactusDiseaseValue()) + if(isDead) setVisualState(getCactusDeathValue()) + else if(isDiseased && !isDead) setVisualState(getCactusDiseaseValue()) } PatchType.HERB -> { - if(isDead) setVarbit(player, patch.varbit, getHerbDeathValue()) - else if(isDiseased && !isDead) setVarbit(player, patch.varbit, getHerbDiseaseValue()) - else setVarbit(player, patch.varbit, (plantable?.value ?: 0) + currentGrowthStage) + if(isDead) setVisualState(getHerbDeathValue()) + else if(isDiseased && !isDead) setVisualState(getHerbDiseaseValue()) + else setVisualState((plantable?.value ?: 0) + currentGrowthStage) } else -> {} } diff --git a/Server/src/main/content/global/skill/farming/UseWithPatchHandler.kt b/Server/src/main/content/global/skill/farming/UseWithPatchHandler.kt index c0eeb7a4b..02e02fcb6 100644 --- a/Server/src/main/content/global/skill/farming/UseWithPatchHandler.kt +++ b/Server/src/main/content/global/skill/farming/UseWithPatchHandler.kt @@ -25,11 +25,10 @@ class UseWithPatchHandler : InteractionListener { @JvmField val allowedNodes = ArrayList() - val patches = ArrayList() override fun defineListeners() { loadNodes() - onUseWith(IntType.SCENERY, allowedNodes.toIntArray(), *patches.toIntArray()) { player, used, with -> + onUseWith(IntType.SCENERY, allowedNodes.toIntArray(), *FarmingPatch.patchNodes.toIntArray()) { player, used, with -> val patch = FarmingPatch.forObject(with.asScenery()) ?: return@onUseWith true val usedItem = used.asItem() @@ -233,23 +232,6 @@ class UseWithPatchHandler : InteractionListener { } fun loadNodes(){ - patches.addAll(8550..8557) //allotment wrappers - patches.addAll(7847..7853) //flower patch wrappers - patches.addAll(8150..8156) //herb patch wrappers - patches.addAll(8388..8391) // Tree patches - patches.add(19147) //Tree patch - patches.addAll(7962..7965) //fruit trees - patches.addAll(8173..8176) //hops - patches.addAll(7577..7580) //bush - patches.add(23760) //evil turnip - patches.add(7572) //belladonna - patches.add(8337) //mushroom - patches.add(27197) //jade vine - patches.add(7771) //cactus - patches.add(7807) //calquat - patches.addAll(8382..8383)//spirit trees - patches.add(8338) //spirit tree - patches.add(18816) //death plateau wrapper for(p in Plantable.values()){ allowedNodes.add(p.itemID) } diff --git a/Server/src/main/core/cache/def/impl/SceneryDefinition.java b/Server/src/main/core/cache/def/impl/SceneryDefinition.java index 909345a9e..60ba83f99 100644 --- a/Server/src/main/core/cache/def/impl/SceneryDefinition.java +++ b/Server/src/main/core/cache/def/impl/SceneryDefinition.java @@ -896,15 +896,23 @@ public class SceneryDefinition extends Definition { } else { configValue = 0; } - if (configValue < 0 || configValue >= childrenIds.length - 1 || childrenIds[configValue] == -1) { + SceneryDefinition childDef = getChildObjectAtIndex(configValue); + if (childDef != null) childDef.configFileId = this.configFileId; + return childDef; + } + + public SceneryDefinition getChildObjectAtIndex (int index) { + if (childrenIds == null || childrenIds.length < 1) { + return this; + } + if (index < 0 || index >= childrenIds.length - 1 || childrenIds[index] == -1) { int objectId = childrenIds[childrenIds.length - 1]; if (objectId != -1) { return forId(objectId); } return this; } - forId(childrenIds[configValue]).configFileId = this.configFileId; - return forId(childrenIds[configValue]); + return forId(childrenIds[index]); } /** diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt index 1d994315f..20d7d28f0 100644 --- a/Server/src/test/kotlin/TestUtils.kt +++ b/Server/src/test/kotlin/TestUtils.kt @@ -1,4 +1,5 @@ import content.global.ame.RandomEventManager +import content.global.skill.farming.timers.CropGrowth import core.cache.Cache import core.cache.crypto.ISAACCipher import core.cache.crypto.ISAACPair @@ -83,6 +84,7 @@ object TestUtils { fun registerTimers() { //allow timers to be registered for use by tests TimerRegistry.registerTimer(Poison()) TimerRegistry.registerTimer(Disease()) + TimerRegistry.registerTimer(CropGrowth()) } fun loadFile(path: String) : URI? { diff --git a/Server/src/test/kotlin/content/skill/farming/PatchTests.kt b/Server/src/test/kotlin/content/skill/farming/PatchTests.kt new file mode 100644 index 000000000..2096b3638 --- /dev/null +++ b/Server/src/test/kotlin/content/skill/farming/PatchTests.kt @@ -0,0 +1,81 @@ +package content.skill.farming + +import TestUtils +import content.global.skill.farming.FarmingPatch +import content.global.skill.farming.PatchRaker +import content.global.skill.farming.PatchType +import content.global.skill.farming.Plantable +import content.global.skill.farming.timers.CropGrowth +import core.cache.def.impl.SceneryDefinition +import core.game.node.entity.skill.Skills +import core.game.system.timer.TimerRegistry +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class PatchTests { + init { TestUtils.preTestSetup() } + @Test fun patchGettingDiseasedOrDyingShouldNotGoIntoInvalidState() { + //Dead/Watered (state | 0x40) and Diseased (state | 0x80) are both invalid stages for limpwurt when fully grown (stage 4/state 32), so we use this as our testcase + //specifically, limpwurt has an issue when the patch is ONLY watered (state | 0x40) OR diseased (state | 0x80), having both (state | 0x40 | 0x80) is fine. + TestUtils.getMockPlayer("invalidPatchState").use {p -> + p.skills.setStaticLevel(Skills.FARMING, 99) + p.skills.setLevel(Skills.FARMING, 99) + + val patch = FarmingPatch.S_FALADOR_FLOWER_C.getPatchFor(p) + patch.plant(Plantable.LIMPWURT_SEED) + + for (i in 0 until 4) { //grow to full growth while making sure we don't die/get diseased + patch.currentGrowthStage = 4 + patch.update() + } + + Assertions.assertEquals(32, patch.getCurrentState()) + + patch.isWatered = true + patch.update() + Assertions.assertEquals(false, patch.isDiseased) + Assertions.assertEquals(false, patch.isDead) + Assertions.assertEquals(false, patch.isWatered) + Assertions.assertEquals(32, patch.getCurrentState()) + + patch.isDiseased = true + patch.updateBit() + Assertions.assertEquals(false, patch.isDiseased) + Assertions.assertEquals(false, patch.isDead) + Assertions.assertEquals(false, patch.isWatered) + Assertions.assertEquals(32, patch.getCurrentState()) + } + } + + @Test fun rakingPatchAtInvalidStagesShouldDoNothing() { + TestUtils.getMockPlayer("Patchraker").use {p -> + val patch = FarmingPatch.S_FALADOR_FLOWER_C.getPatchFor(p) + patch.update() + + Assertions.assertEquals(0, patch.getCurrentState()) //assert that it's in the max weedy state + + PatchRaker.rake(p, patch.patch) + TestUtils.advanceTicks(25, false) + Assertions.assertEquals(3, patch.getCurrentState()) + + PatchRaker.rake(p, patch.patch) + TestUtils.advanceTicks(10, false) + Assertions.assertEquals(3, patch.getCurrentState()) + + patch.plant(Plantable.POTATO_SEED) + val plantedBaseline = patch.getCurrentState() + PatchRaker.rake(p, patch.patch) + TestUtils.advanceTicks(10, false) + Assertions.assertEquals(plantedBaseline, patch.getCurrentState()) + + patch.currentGrowthStage = Plantable.POTATO_SEED.stages + patch.update() + Assertions.assertEquals(true, patch.isGrown()) + + val stateBaseline = patch.getCurrentState() + PatchRaker.rake(p, patch.patch) + TestUtils.advanceTicks(10, false) + Assertions.assertEquals(stateBaseline, patch.getCurrentState()) + } + } +} \ No newline at end of file