From 40a495fcdc68c50d016355ac67a16ad8696e4226 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 13:44:43 +0000 Subject: [PATCH 01/23] Fixed enchanted bolts not working against monsters that cannot be slayer tasks --- .../main/core/game/node/entity/combat/equipment/BoltEffect.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java b/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java index 06ab0edf2..71d26d8b6 100644 --- a/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java +++ b/Server/src/main/core/game/node/entity/combat/equipment/BoltEffect.java @@ -239,7 +239,7 @@ public enum BoltEffect { // and should be considered a TODO: temporary abuse prevention measure (bolt effect on slayer monsters) boolean rollSuccess = RandomFunction.random(13) == 5; if (!(state.getVictim() instanceof NPC)) return rollSuccess; - if (!(state.getAttacker() instanceof Player) && ((NPC) state.getVictim()).getTask() == null) return rollSuccess; + if (!(state.getAttacker() instanceof Player) || ((NPC) state.getVictim()).getTask() == null) return rollSuccess; if (state.getVictim().asNpc().getTask().levelReq > state.getAttacker().asPlayer().getSkills().getLevel(Skills.SLAYER)) return false; return rollSuccess; } From 626a3d0f80708b95d769e3c630914668d8442aa5 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Thu, 14 May 2026 20:24:33 -0400 Subject: [PATCH 02/23] Make it possible to eat caviar --- Server/src/main/content/data/consumables/Consumables.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/src/main/content/data/consumables/Consumables.java b/Server/src/main/content/data/consumables/Consumables.java index 2d3744679..3730caf32 100644 --- a/Server/src/main/content/data/consumables/Consumables.java +++ b/Server/src/main/content/data/consumables/Consumables.java @@ -317,6 +317,7 @@ public enum Consumables { TCHIKI_MONKEY_PASTE(new Food(new int[] {7575}, new HealingEffect(5), "You eat the Tchiki monkey nut paste. It sticks to the roof of your mouth.")), OOMLIE_WRAP(new Food(new int[] {Items.COOKED_OOMLIE_WRAP_2343}, new MultiEffect(new HealingEffect(14), new AchievementEffect(DiaryType.KARAMJA, 2, 2)))), ROE(new Food(new int[]{11324}, new HealingEffect(3))), + CAVIAR(new Food(new int[]{11326}, new HealingEffect(5))), EQUA_LEAVES(new Food(new int[]{2128}, new HealingEffect(1))), CHOC_ICE(new Food(new int[]{6794}, new HealingEffect(7))), EDIBLE_SEAWEED(new Food(new int[] {403}, new HealingEffect(4))), From dd1cfbba515dd4310e2d56ae4cd28834456978ee Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:13:28 +0000 Subject: [PATCH 03/23] Combo runes are now consumed before standard elemental runes if both are present in inventory 1 combo rune can count for 2 elemental runes if the spell requires both --- .../global/skill/magic/SpellListener.kt | 9 +- .../content/global/skill/magic/SpellUtils.kt | 103 ++++++++---------- .../skill/magic/modern/ModernListeners.kt | 11 +- .../node/entity/combat/spell/MagicSpell.java | 60 ++-------- 4 files changed, 63 insertions(+), 120 deletions(-) diff --git a/Server/src/main/content/global/skill/magic/SpellListener.kt b/Server/src/main/content/global/skill/magic/SpellListener.kt index 64383b0a7..e938f3ef0 100644 --- a/Server/src/main/content/global/skill/magic/SpellListener.kt +++ b/Server/src/main/content/global/skill/magic/SpellListener.kt @@ -47,11 +47,10 @@ abstract class SpellListener(val bookName: String) : Listener { player.sendMessage("You need a magic level of $magicLevel to cast this spell.") throw IllegalStateException() } - for(rune in runes){ - if(!SpellUtils.hasRune(player,rune)){ - player.sendMessage("You don't have enough ${rune.definition.name.lowercase()}s to cast this spell.") - throw IllegalStateException() - } + val missing = SpellUtils.hasRunes(player, runes) + if (missing != null) { + player.sendMessage("You don't have enough ${missing.definition.name.lowercase()}s to cast this spell.") + throw IllegalStateException() } for(item in specialEquipment){ if(!player.equipment.contains(item,1)){ diff --git a/Server/src/main/content/global/skill/magic/SpellUtils.kt b/Server/src/main/content/global/skill/magic/SpellUtils.kt index beafbf64d..57e8b4c6d 100644 --- a/Server/src/main/content/global/skill/magic/SpellUtils.kt +++ b/Server/src/main/content/global/skill/magic/SpellUtils.kt @@ -3,9 +3,9 @@ package content.global.skill.magic import core.game.node.entity.combat.spell.CombinationRune import core.game.node.entity.combat.spell.MagicStaff import core.game.node.entity.combat.spell.Runes -import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.game.node.item.Item +import kotlin.math.min object SpellUtils { /** @@ -32,70 +32,57 @@ object SpellUtils { return false } - fun hasRune(p:Player,rune:Item):Boolean{ - val removeItems = p.getAttribute("spell:runes",ArrayList()) - if(usingStaff(p,rune.id)) return true - if(p.inventory.containsItem(rune)){ - removeItems.add(rune) - p.setAttribute("spell:runes",removeItems) + /** + * Validates if the player has the necessary runes to cast a spell. + * + * If the player is able to cast the spell, the "spell:runes" attribute will be set to the list of items that should + * be removed from the player's inventory after successfully casting the spell. This accounts for staves and + * combination runes. + * + * @param p The player casting the spell + * @param runes The runes and other items required to cast the spell + * @return null if the player can cast the spell or an Item representing at least one of the runes the player is missing + */ + @JvmStatic + fun hasRunes(p: Player, runes: Array): Item? { + val cost = HashMap() + // `runes` are mostly actual runes but occasionally other items like staves or unpowered orbs + for (rune in runes) { + if (usingStaff(p, rune.id)) continue + cost[rune.id] = cost.getOrDefault(rune.id, 0) + rune.amount } - val baseAmt = p.inventory.getAmount(rune.id) - var amtRemaining = rune.amount - baseAmt - val possibleComboRunes = CombinationRune.eligibleFor(Runes.forId(rune.id)) - for (r in possibleComboRunes) { - if (p.inventory.containsItem(Item(r.id)) && amtRemaining > 0) { - val amt = p.inventory.getAmount(r.id) - if (amtRemaining <= amt) { - removeItems.add(Item(r.id,amtRemaining)) - amtRemaining = 0 - break - } - removeItems.add(Item(r.id,p.inventory.getAmount(r.id))) - amtRemaining -= p.inventory.getAmount(r.id) + val toRemove = ArrayList() + + // Combination runes are used before elemental runes. + // https://runescape.wiki/w/Runecrafting?oldid=2618332#Function_and_Usage_of_Combination_Runes: + for (combo in CombinationRune.values()) { + val available = p.inventory.getAmount(combo.id) + val maxUsage = combo.types.mapNotNull { cost[it.id] }.maxOrNull() ?: 0 + if (maxUsage > 0 && available >= 0) { + val usage = min(maxUsage, available) + + toRemove.add(Item(combo.id, usage)) + // Even if a spell uses both parts of a combo rune, it should only consume a single rune. For example, + // a spell that requires an air rune and an earth rune should only consume a single dust rune. + // https://youtu.be/9gAiqEmF-Hc?t=67 + combo.types.forEach { cost[it.id] = cost.getOrDefault(it.id, 0) - usage } } } - p.setAttribute("spell:runes",removeItems) - return amtRemaining <= 0 - } - fun hasRune(p: Player, item: Item, toRemove: MutableList, message: Boolean): Boolean { - if (!usingStaff(p, item.id)) { - val hasBaseRune = p.inventory.contains(item.id, item.amount) - if (!hasBaseRune) { - val baseAmt = p.inventory.getAmount(item.id) - if (baseAmt > 0) { - toRemove.add(Item(item.id, p.inventory.getAmount(item.id))) - } - var amtRemaining = item.amount - baseAmt - val possibleComboRunes = CombinationRune.eligibleFor(Runes.forId(item.id)) - for (r in possibleComboRunes) { - if (p.inventory.containsItem(Item(r.id)) && amtRemaining > 0) { - val amt = p.inventory.getAmount(r.id) - if (amtRemaining < amt) { - toRemove.add(Item(r.id, amtRemaining)) - amtRemaining = 0 - continue - } - amtRemaining -= p.inventory.getAmount(r.id) - toRemove.add(Item(r.id, p.inventory.getAmount(r.id))) - } - } - return if (amtRemaining <= 0) { - true - } else { - p.packetDispatch.sendMessage("You don't have enough " + item.name + "s to cast this spell.") - false - } + for ((runeId, amount) in cost) { + if (amount <= 0) continue + + val available = p.inventory.getAmount(runeId) + if (available < amount) { + return Item(runeId, amount) } - toRemove.add(item) - return true - } - return true - } - fun attackableNPC(npc: NPC): Boolean{ - return npc.definition.hasAction("attack") + toRemove.add(Item(runeId, amount)) + } + + p.setAttribute("spell:runes", toRemove) + return null } @JvmStatic diff --git a/Server/src/main/content/global/skill/magic/modern/ModernListeners.kt b/Server/src/main/content/global/skill/magic/modern/ModernListeners.kt index 914b06646..777beee18 100644 --- a/Server/src/main/content/global/skill/magic/modern/ModernListeners.kt +++ b/Server/src/main/content/global/skill/magic/modern/ModernListeners.kt @@ -2,7 +2,7 @@ package content.global.skill.magic.modern import content.data.Quests import content.global.skill.magic.SpellListener -import content.global.skill.magic.SpellUtils.hasRune +import content.global.skill.magic.SpellUtils.hasRunes import content.global.skill.magic.TeleportMethod import content.global.skill.magic.homeTeleport import content.global.skill.magic.spellconsts.Modern @@ -323,11 +323,10 @@ class ModernListeners : SpellListener("modern"){ sendMessage(player, "You need a magic level of ${spell.level} to cast this spell.") return@queueScript stopExecuting(player) } - for (rune in spell.requiredRunes) { - if(!hasRune(player,rune)){ - sendMessage(player, "You don't have enough ${rune.name.lowercase()}s to cast this spell.") - return@queueScript stopExecuting(player) - } + val missing = hasRunes(player, spell.requiredRunes) + if (missing != null) { + sendMessage(player, "You don't have enough ${missing.name.lowercase()}s to cast this spell.") + return@queueScript stopExecuting(player) } visualizeSpell(player, CHARGE_ORB_ANIM, spell.graphics, spell.sound) removeRunes(player) diff --git a/Server/src/main/core/game/node/entity/combat/spell/MagicSpell.java b/Server/src/main/core/game/node/entity/combat/spell/MagicSpell.java index 896a3ec62..544372bba 100644 --- a/Server/src/main/core/game/node/entity/combat/spell/MagicSpell.java +++ b/Server/src/main/core/game/node/entity/combat/spell/MagicSpell.java @@ -1,5 +1,6 @@ package core.game.node.entity.combat.spell; +import content.global.skill.magic.SpellUtils; import core.game.event.SpellCastEvent; import core.game.node.Node; import core.game.node.entity.Entity; @@ -17,7 +18,6 @@ import core.plugin.Plugin; import core.tools.RandomFunction; import java.util.ArrayList; -import java.util.List; import static core.api.ContentAPIKt.playGlobalAudio; @@ -223,16 +223,17 @@ public abstract class MagicSpell implements Plugin { if (runes == null) { return true; } - List toRemove = new ArrayList<>(20); - for (Item item : runes) { - if (!hasRune(p, item, toRemove, message)) { - return false; + Item missing = SpellUtils.hasRunes(p, runes); + if (missing != null) { + if (message) { + p.getPacketDispatch().sendMessage("You don't have enough " + missing.getName() + "s to cast this spell."); } + return false; } if (remove) { - toRemove.forEach(i -> { - p.getInventory().remove(i); - }); + ArrayList toRemove = p.getAttribute("spell:runes", new ArrayList<>()); + toRemove.forEach(i -> p.getInventory().remove(i)); + p.removeAttribute("spell:runes"); } return true; } @@ -255,49 +256,6 @@ public abstract class MagicSpell implements Plugin { return true; } - /** - * Checks if the player has a rune to remove. - * @param p the player. - * @param item the item. - * @param toRemove the list of items to remove. - * @param message the message. - * @return {@code True} if so. - */ - public boolean hasRune(Player p, Item item, List toRemove, boolean message) { - if (!usingStaff(p, item.getId())) { - boolean hasBaseRune = p.getInventory().contains(item.getId(),item.getAmount()); - if(!hasBaseRune){ - int baseAmt = p.getInventory().getAmount(item.getId()); - if(baseAmt > 0){ - toRemove.add(new Item(item.getId(),p.getInventory().getAmount(item.getId()))); - } - int amtRemaining = item.getAmount() - baseAmt; - List possibleComboRunes = CombinationRune.eligibleFor(Runes.forId(item.getId())); - for(CombinationRune r : possibleComboRunes){ - if(p.getInventory().containsItem(new Item(r.id)) && amtRemaining > 0){ - int amt = p.getInventory().getAmount(r.id); - if(amtRemaining < amt){ - toRemove.add(new Item(r.id,amtRemaining)); - amtRemaining = 0; - continue; - } - amtRemaining -= p.getInventory().getAmount(r.id); - toRemove.add(new Item(r.id,p.getInventory().getAmount(r.id))); - } - } - if(amtRemaining <= 0){ - return true; - } else { - p.getPacketDispatch().sendMessage("You don't have enough " + item.getName() + "s to cast this spell."); - return false; - } - } - toRemove.add(item); - return true; - } - return true; - } - /** * Adds the experience for casting this spell. * @param entity The entity to reward with experience. From 0c4cfcb1ebfb94bc4fbf2cb6515811623497eb40 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:13:57 +0000 Subject: [PATCH 04/23] Fixed salamanders and swamp lizard throwing in ranged mode --- .../data/configs/ranged_weapon_configs.json | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Server/data/configs/ranged_weapon_configs.json b/Server/data/configs/ranged_weapon_configs.json index 335c9f031..dc6fbe429 100644 --- a/Server/data/configs/ranged_weapon_configs.json +++ b/Server/data/configs/ranged_weapon_configs.json @@ -1546,5 +1546,41 @@ "animation": "426", "drop_ammo": "true", "ammunition": "14202,14203,14204,14205,14206" + }, + { + "itemId": "10149", + "name": "Swamp lizard", + "ammo_slot": "13", + "weapon_type": "0", + "animation": "5247", + "drop_ammo": "false", + "ammunition": "10142" + }, + { + "itemId": "10146", + "name": "Orange salamander", + "ammo_slot": "13", + "weapon_type": "0", + "animation": "5247", + "drop_ammo": "false", + "ammunition": "10143" + }, + { + "itemId": "10147", + "name": "Red salamander", + "ammo_slot": "13", + "weapon_type": "0", + "animation": "5247", + "drop_ammo": "false", + "ammunition": "10144" + }, + { + "itemId": "10148", + "name": "Black salamander", + "ammo_slot": "13", + "weapon_type": "0", + "animation": "5247", + "drop_ammo": "false", + "ammunition": "10145" } ] From dec9c04fdd5d9840f586ef85b48926a57d649f2f Mon Sep 17 00:00:00 2001 From: Player Name Date: Sat, 30 May 2026 14:15:18 +0000 Subject: [PATCH 05/23] Rewrote wilderness agility course lava stones --- .../global/skill/agility/WildernessCourse.kt | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/Server/src/main/content/global/skill/agility/WildernessCourse.kt b/Server/src/main/content/global/skill/agility/WildernessCourse.kt index 00f08cf31..d48d00a1a 100644 --- a/Server/src/main/content/global/skill/agility/WildernessCourse.kt +++ b/Server/src/main/content/global/skill/agility/WildernessCourse.kt @@ -2,6 +2,7 @@ package content.global.skill.agility import core.api.* import core.cache.def.impl.SceneryDefinition +import core.game.interaction.QueueStrength import core.game.node.Node import core.game.node.scenery.Scenery import core.game.node.entity.player.Player @@ -168,25 +169,27 @@ class WildernessCourse private fun handleSteppingStones(player: Player, `object`: Scenery) { lock(player, 50) val fail = AgilityHandler.hasFailed(player, 1, 0.3) - val origLoc = player.location - registerLogoutListener(player, "steppingstone"){p -> - player.location = origLoc - } - submitWorldPulse(object : Pulse(2, player){ - var counter = 0 - override fun pulse(): Boolean { - if (counter == 3 && fail) { - AgilityHandler.fail(player, -1, Location.create(3001, 3963, 0), Animation.create(771), (player.skills.lifepoints * 0.26).toInt(), "...You lose your footing and fall into the lava.") - return true - } - AgilityHandler.forceWalk(player, if (counter == 5) 2 else -1, player.location, player.location.transform(-1, 0, 0), Animation.create(741), 10, if (counter == 5) 20.0 else 0.0, if (counter != 0) null else "You carefully start crossing the stepping stones...") - if(++counter == 6){ - unlock(player) - clearLogoutListener(player, "steppingstone") - } - return counter == 6 + queueScript(player, 0, QueueStrength.SOFT) { stage -> + val courseIndex = if (stage == 5) 2 else -1 + val start = player.location + val end = player.location.transform(-1, 0, 0) + val anim = Animation(741) + val xp = if (stage == 5) 20.0 else 0.0 + val message = if (stage == 0) "You carefully start crossing the stepping stones..." else null + AgilityHandler.forceWalk(player, courseIndex, start, end, anim, 10, xp, message) + if (stage == 2 && fail) { + val dest = Location(3001, 3963, 0) + val failanim = Animation(771) + val hit = (player.skills.lifepoints * 0.26).toInt() + AgilityHandler.fail(player, -1, dest, failanim, hit, "... You lose your footing and fall into the lava.") + return@queueScript stopExecuting(player) } - }) + if (stage == 5) { + unlock(player) + return@queueScript stopExecuting(player) + } + return@queueScript delayScript(player, 2) + } } /** From e66be1b9d1c0c5db8a1f6b00025f6868564ba171 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:15:30 +0000 Subject: [PATCH 06/23] Improved authenticity of Father Urhney dialog --- .../FatherUhrneyDialogue.java | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/Server/src/main/content/region/misthalin/lumbridge/quest/therestlessghost/FatherUhrneyDialogue.java b/Server/src/main/content/region/misthalin/lumbridge/quest/therestlessghost/FatherUhrneyDialogue.java index 1097d1d5e..219c0c562 100644 --- a/Server/src/main/content/region/misthalin/lumbridge/quest/therestlessghost/FatherUhrneyDialogue.java +++ b/Server/src/main/content/region/misthalin/lumbridge/quest/therestlessghost/FatherUhrneyDialogue.java @@ -1,6 +1,7 @@ package content.region.misthalin.lumbridge.quest.therestlessghost; import content.data.Quests; +import core.game.dialogue.FacialExpression; import core.game.node.entity.npc.NPC; import core.game.node.entity.player.Player; import core.game.node.entity.player.link.diary.DiaryType; @@ -41,7 +42,7 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { @Override public boolean open(Object... args) { npc = (NPC) args[0]; - npc("Go away! I'm meditating!"); + npc(FacialExpression.ANGRY, "Go away! I'm meditating!"); stage = 0; return true; } @@ -51,13 +52,13 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { switch (stage) { case 0: if (player.getQuestRepository().getQuest(Quests.THE_RESTLESS_GHOST).getStage(player) == 0) { - options("Well, that's friendly.", "I've come to respossess your house."); + options("Well, that's friendly.", "I've come to repossess your house."); stage = 1; } else if (player.getQuestRepository().getQuest(Quests.THE_RESTLESS_GHOST).getStage(player) == 10) { - options("Well, that's friendly.", "I've come to respossess your house.", "Father Aereck sent me to talk to you."); + options("Well, that's friendly.", "Father Aereck sent me to talk to you.", "I've come to repossess your house."); stage = 500; } else if (player.getGameAttributes().getAttributes().containsKey("restless-ghost:urhney") || player.getQuestRepository().isComplete(Quests.THE_RESTLESS_GHOST)) { - options("Well, that's friendly.", "I've come to respossess your house.", "I've lost the Amulet of Ghostspeak."); + options("Well, that's friendly.", "I've lost the Amulet of Ghostspeak.", "I've come to repossess your house."); stage = 514; } break; @@ -68,17 +69,17 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { stage = 10; break; case 2: - player("I've come to repossess your house."); - stage = 20; - break; - case 3: player("Father Aereck sent me to talk to you."); stage = 501; break; + case 3: + player("I've come to repossess your house."); + stage = 20; + break; } break; case 501: - npc("I suppose I'd better talk to you then. What problems", "has he got himself into this time?"); + npc(FacialExpression.ANGRY, "I suppose I'd better talk to you then. What problems", "has he got himself into this time?"); stage = 502; break; case 502: @@ -86,23 +87,23 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { stage = 503; break; case 503: - npc("Oh, the silly fool."); + npc(FacialExpression.ANGRY, "Oh, the silly fool."); stage = 504; break; case 504: - npc("I leave town for just five months, and ALREADY he", "can't manage."); + npc(FacialExpression.ANGRY, "I leave town for just five months, and ALREADY he", "can't manage."); stage = 505; break; case 505: - npc("(sigh)"); + npc(FacialExpression.SAD, "(sigh)"); stage = 506; break; case 506: - npc("Well, I can't go back and exorcise it. I vowed not to", "leave this place. Until I had done a full two years of", "prayer and meditation."); + npc(FacialExpression.ANGRY, "Well, I can't go back and exorcise it. I vowed not to", "leave this place. Until I had done a full two years of", "prayer and meditation."); stage = 507; break; case 507: - npc("Tell you what I can do though; take this amulet."); + npc(FacialExpression.NEUTRAL, "Tell you what I can do though; take this amulet."); stage = 508; break; case 508: @@ -118,15 +119,15 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { stage = 509; break; case 509: - npc("It is an Amulet of Ghostspeak."); + npc(FacialExpression.NEUTRAL, "It is an Amulet of Ghostspeak."); stage = 510; break; case 510: - npc("So called, because when you wear it you can speak to", "ghosts. A lot of ghosts are doomed to be ghosts because", "they have left some important task uncompleted."); + npc(FacialExpression.NEUTRAL, "So called, because when you wear it you can speak to", "ghosts. A lot of ghosts are doomed to be ghosts because", "they have left some important task uncompleted."); stage = 511; break; case 511: - npc("Maybe if you know what this task is, you can get rid of", "the ghost. I'm not making any gurantees mind you,", "but it is the best I can do right now."); + npc(FacialExpression.NEUTRAL, "Maybe if you know what this task is, you can get rid of", "the ghost. I'm not making any guarantees mind you,", "but it is the best I can do right now."); stage = 512; break; case 512: @@ -143,12 +144,12 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { stage = 10; break; case 2: - player("I've come to repossess your house."); - stage = 20; + player(FacialExpression.NEUTRAL, "I've lost the Amulet of Ghostspeak."); + stage = 515; break; case 3: - player("I've lost the Amulet of Ghostpeak."); - stage = 515; + player("I've come to repossess your house."); + stage = 20; break; } break; @@ -171,14 +172,14 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { stage = 518; break; case 517: - npc("What are you talking about? I can see you've got it", "in your bank!"); + npc(FacialExpression.ANGRY, "You come here wasting my time... Has it even", "occurred to you to look in your bank? Now GO", "AWAY!"); stage = 518; break; case 518: end(); break; case 519: - npc("How careless can you get? Those things aren't easy to", "come by you know! It's a good job I've got a spare."); + npc(FacialExpression.ANGRY, "How careless can you get? Those things aren't easy to", "come by you know! It's a good job I've got a spare."); stage = 520; break; case 520: @@ -188,7 +189,7 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { stage = 521; break; case 521: - npc("Be more careful this time."); + npc(FacialExpression.ANGRY, "Be more careful this time."); stage = 522; break; case 522: @@ -252,7 +253,7 @@ public final class FatherUhrneyDialogue extends DialoguePlugin { stage = 102; break; case 102: - player("Sorry. I mus thave got the wrong address. All the", "houses look the same around here."); + player("Sorry. I must have got the wrong address. All the", "houses look the same around here."); stage = 103; break; case 103: From 4853262c2549ea4cf0f34adbce971dee7d60f0cf Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:15:56 +0000 Subject: [PATCH 07/23] Fixed Lumbridge teleport achievement being granted for Miasmic Burst --- .../lumbridge/diary/LumbridgeAchivementDiary.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Server/src/main/content/region/misthalin/lumbridge/diary/LumbridgeAchivementDiary.kt b/Server/src/main/content/region/misthalin/lumbridge/diary/LumbridgeAchivementDiary.kt index 5a8ca76db..965dcb157 100644 --- a/Server/src/main/content/region/misthalin/lumbridge/diary/LumbridgeAchivementDiary.kt +++ b/Server/src/main/content/region/misthalin/lumbridge/diary/LumbridgeAchivementDiary.kt @@ -20,6 +20,7 @@ import core.game.diary.DiaryEventHookBase import core.game.diary.DiaryLevel import core.game.event.* import content.data.Quests +import core.game.node.entity.player.link.SpellBookManager class LumbridgeAchivementDiary : DiaryEventHookBase(DiaryType.LUMBRIDGE) { @@ -401,14 +402,12 @@ class LumbridgeAchivementDiary : DiaryEventHookBase(DiaryType.LUMBRIDGE) { } override fun onSpellCast(player: Player, event: SpellCastEvent) { - when (event.spellId) { - Modern.LUMBRIDGE_TELEPORT -> { - finishTask( - player, - DiaryLevel.MEDIUM, - MediumTasks.CAST_LUMBRIDGE_TELEPORT - ) - } + if (event.spellBook == SpellBookManager.SpellBook.MODERN && event.spellId == Modern.LUMBRIDGE_TELEPORT) { + finishTask( + player, + DiaryLevel.MEDIUM, + MediumTasks.CAST_LUMBRIDGE_TELEPORT + ) } } From 4795f25c09fa3489a440fd90c9e16728331e8e3a Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:18:16 +0000 Subject: [PATCH 08/23] Improved bounding box to get Falador diary achievement for lighting a bullseye lantern anywhere in Chemist's house --- .../region/asgarnia/falador/diary/FaladorAchievementDiary.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/region/asgarnia/falador/diary/FaladorAchievementDiary.kt b/Server/src/main/content/region/asgarnia/falador/diary/FaladorAchievementDiary.kt index 4831afd9c..aac922c9a 100644 --- a/Server/src/main/content/region/asgarnia/falador/diary/FaladorAchievementDiary.kt +++ b/Server/src/main/content/region/asgarnia/falador/diary/FaladorAchievementDiary.kt @@ -33,7 +33,7 @@ class FaladorAchievementDiary : DiaryEventHookBase(DiaryType.FALADOR) { private val WAYNES_CHAINS_AREA = ZoneBorders(2969, 3310, 2975, 3314) private val SARAHS_FARMING_SHOP_AREA = ZoneBorders(3021, 3285, 3040, 3296) private val FALADOR_GENERAL_AREA = ZoneBorders(2934, 3399, 3399, 3307) - private val CHEMIST_AREA = ZoneBorders(2929, 3213, 2936, 3207) + private val CHEMIST_AREA = ZoneBorders(2925, 3213, 2939, 3207) private val PORT_SARIM_FLOWER_PATCH = ZoneBorders(3053, 3306, 3056, 3309) From bd5ce18fc19f07b9d0630d6b80c89e6252abdca3 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:22:50 +0000 Subject: [PATCH 09/23] Fixed Cyclops defence animation Fixed Zombie animations in Ardy sewers Fixed Ghost combat animations in Draynor Manor Fixed potion drinking animation and add two tick potion text delay Fixed Gnome death animation in Khazard Battlefield Fixed Gnome animations in Tree Gnome Village Removed inauthentic + broken Vinesweeper teleport animation --- Server/data/configs/npc_configs.json | 54 ++++++++++++------- .../minigame/vinesweeper/Vinesweeper.kt | 3 +- .../src/main/core/game/consumable/Potion.java | 26 +++++---- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/Server/data/configs/npc_configs.json b/Server/data/configs/npc_configs.json index 96b4883b6..816287ac7 100644 --- a/Server/data/configs/npc_configs.json +++ b/Server/data/configs/npc_configs.json @@ -1264,12 +1264,12 @@ }, { "examine": "Like a mini man!", - "melee_animation": "422", - "range_animation": "422", + "melee_animation": "190", + "range_animation": "190", "magic_level": "1", - "defence_animation": "404", + "defence_animation": "193", "magic_animation": "422", - "death_animation": "9055", + "death_animation": "196", "name": "Gnome", "defence_level": "1", "safespot": null, @@ -1281,12 +1281,12 @@ }, { "examine": "Like a mini man!", - "melee_animation": "422", - "range_animation": "422", + "melee_animation": "190", + "range_animation": "190", "magic_level": "1", - "defence_animation": "404", + "defence_animation": "193", "magic_animation": "422", - "death_animation": "9055", + "death_animation": "196", "name": "Gnome", "defence_level": "1", "safespot": null, @@ -1298,12 +1298,12 @@ }, { "examine": "Like a mini man!", - "melee_animation": "422", - "range_animation": "422", + "melee_animation": "190", + "range_animation": "190", "magic_level": "1", - "defence_animation": "404", + "defence_animation": "193", "magic_animation": "422", - "death_animation": "9055", + "death_animation": "196", "name": "Gnome", "defence_level": "1", "safespot": null, @@ -1907,17 +1907,17 @@ }, { "examine": "Eeek! A ghost!", - "melee_animation": "5540", + "melee_animation": "5532", "range_animation": "0", "combat_audio": "436,439,438", "attack_speed": "4", "magic_level": "1", "respawn_delay": "40", - "defence_animation": "5541", + "defence_animation": "5533", "weakness": "5", "slayer_exp": "25", "magic_animation": "0", - "death_animation": "5542", + "death_animation": "5534", "name": "Ghost", "defence_level": "18", "safespot": null, @@ -2182,11 +2182,11 @@ "attack_speed": "4", "magic_level": "1", "respawn_delay": "25", - "defence_animation": "360", + "defence_animation": "4651", "weakness": "8", "slayer_exp": "75", "magic_animation": "359", - "death_animation": "361", + "death_animation": "4653", "name": "Cyclops", "defence_level": "35", "safespot": null, @@ -47336,12 +47336,15 @@ "attack_level": "1" }, { + "combat_audio": "931,923,922", + "melee_animation": "5578", + "defence_animation": "5567", "slayer_exp": "30", + "death_animation": "5569", "name": "Zombie", "defence_level": "1", "safespot": null, "lifepoints": "40", - "combat_audio": "931,923,922", "strength_level": "1", "id": "5394", "range_level": "1", @@ -76045,8 +76048,21 @@ "id": "2239" }, { + "examine": "Like a mini man!", + "combat_style": "1", + "force_talk": "", + "melee_animation": "190", + "attack_speed": "4", + "respawn_delay": "60", + "defence_animation": "193", + "death_animation": "196", "name": "Gnome", - "id": "2251" + "defence_level": "1", + "lifepoints": "3", + "strength_level": "1", + "id": "2251", + "range_level": "1", + "attack_level": "1" }, { "name": "Crow", diff --git a/Server/src/main/content/minigame/vinesweeper/Vinesweeper.kt b/Server/src/main/content/minigame/vinesweeper/Vinesweeper.kt index 9fe119605..04fe7686a 100644 --- a/Server/src/main/content/minigame/vinesweeper/Vinesweeper.kt +++ b/Server/src/main/content/minigame/vinesweeper/Vinesweeper.kt @@ -523,8 +523,7 @@ class Vinesweeper : InteractionListener, InterfaceListener, MapArea { sendNPCDialogue(player, npc.id, "I can't do that, you're teleblocked!", core.game.dialogue.FacialExpression.OLD_ANGRY1) return } - npc.animate(Animation(437)) - npc.faceTemporary(player, 1) + // https://youtu.be/61jVjmXf8tU?t=45 npc.graphics(Graphics(108)) player.lock() playAudio(player, Sounds.CURSE_ALL_125, 0, 1) diff --git a/Server/src/main/core/game/consumable/Potion.java b/Server/src/main/core/game/consumable/Potion.java index 60afb00c3..89912d9f1 100644 --- a/Server/src/main/core/game/consumable/Potion.java +++ b/Server/src/main/core/game/consumable/Potion.java @@ -1,12 +1,16 @@ package core.game.consumable; import content.data.consumables.Consumables; +import core.game.interaction.QueueStrength; import core.game.node.entity.player.Player; import core.game.node.entity.player.link.audio.Audio; import core.game.node.item.Item; +import core.game.system.task.Pulse; +import core.game.world.GameWorld; +import core.game.world.update.flag.context.Animation; import org.rs09.consts.Sounds; -import static core.api.ContentAPIKt.playAudio; +import static core.api.ContentAPIKt.*; public class Potion extends Drink { @@ -15,7 +19,7 @@ public class Potion extends Drink { private static final Audio SOUND = new Audio(2401, 1, 1); public Potion(final int[] ids, final ConsumableEffect effect, final String... messages) { - super(ids, effect, messages); + super(ids, effect, new Animation(829), messages); } @Override @@ -55,13 +59,17 @@ public class Potion extends Drink { } final int dosesLeft = ids.length - consumedDoses; player.getPacketDispatch().sendMessage("You drink some of your " + getFormattedName(item) + "."); - if (dosesLeft > 1) { - player.getPacketDispatch().sendMessage("You have " + dosesLeft + " doses of potion left."); - } else if (dosesLeft == 1) { - player.getPacketDispatch().sendMessage("You have 1 dose of potion left."); - } else { - player.getPacketDispatch().sendMessage("You have finished your potion."); - } + // Remaining dosages message should be delayed - https://youtu.be/n6CCf4Rj8Lg?t=79 + queueScript(player, 2, QueueStrength.SOFT, false, (Integer stage) -> { + if (dosesLeft > 1) { + player.getPacketDispatch().sendMessage("You have " + dosesLeft + " doses of potion left."); + } else if (dosesLeft == 1) { + player.getPacketDispatch().sendMessage("You have 1 dose of potion left."); + } else { + player.getPacketDispatch().sendMessage("You have finished your potion."); + } + return stopExecuting(player); + }); } public int getDose(Item potion){ From 39ac26f5d97adcd17829bc2aaf56987f84ee6d70 Mon Sep 17 00:00:00 2001 From: Player Name Date: Sat, 30 May 2026 14:24:20 +0000 Subject: [PATCH 10/23] Fixed teleporting into half-loaded POHs --- .../src/main/content/global/skill/construction/HouseZone.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/global/skill/construction/HouseZone.java b/Server/src/main/content/global/skill/construction/HouseZone.java index 8e5a04e85..31a57f6f8 100644 --- a/Server/src/main/content/global/skill/construction/HouseZone.java +++ b/Server/src/main/content/global/skill/construction/HouseZone.java @@ -122,7 +122,7 @@ public final class HouseZone extends MapZone { house.expelGuests(p); int toRemove = previousRegion; int dungRemove = previousDungeon; - submitWorldPulse(new Pulse(2) { + submitWorldPulse(new Pulse(1) { public boolean pulse() { Region r = RegionManager.forId(toRemove); Region dr = dungRemove != -1 ? RegionManager.forId(dungRemove) : null; From 7583f6bff2dc93939638160d6ae7934eee1a38cd Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:25:40 +0000 Subject: [PATCH 11/23] Implemented "Enter the A Soul's Bane Rift" achievement from the Varrock medium diary when entering the rift --- .../region/misthalin/quest/asoulsbane/ASoulsBaneListeners.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/src/main/content/region/misthalin/quest/asoulsbane/ASoulsBaneListeners.kt b/Server/src/main/content/region/misthalin/quest/asoulsbane/ASoulsBaneListeners.kt index 256c7e17d..b19d00484 100644 --- a/Server/src/main/content/region/misthalin/quest/asoulsbane/ASoulsBaneListeners.kt +++ b/Server/src/main/content/region/misthalin/quest/asoulsbane/ASoulsBaneListeners.kt @@ -5,6 +5,7 @@ import core.game.interaction.InteractionListener import core.game.world.map.Location import org.rs09.consts.Scenery import content.data.Quests +import core.game.node.entity.player.link.diary.DiaryType // Temporary access since the monsters in there drop nothing. class ASoulsBaneListener : InteractionListener { @@ -16,6 +17,7 @@ class ASoulsBaneListener : InteractionListener { on(RIFT_IDS, SCENERY, "enter") { player, _ -> if (hasRequirement(player, Quests.A_SOULS_BANE)) { teleport(player, Location(3297, 9824, 0)) + player.achievementDiaryManager.finishTask(player, DiaryType.VARROCK, 1, 9) } return@on true } From 5633fbf1157b17deca54bbd158ecdae0d09d0e9e Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:26:41 +0000 Subject: [PATCH 12/23] Fixed wild pie Varrock hard achievement --- .../misthalin/varrock/dialogue/RomilyWeaklaxDialogue.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/region/misthalin/varrock/dialogue/RomilyWeaklaxDialogue.java b/Server/src/main/content/region/misthalin/varrock/dialogue/RomilyWeaklaxDialogue.java index ed93e45e1..26eac9af2 100644 --- a/Server/src/main/content/region/misthalin/varrock/dialogue/RomilyWeaklaxDialogue.java +++ b/Server/src/main/content/region/misthalin/varrock/dialogue/RomilyWeaklaxDialogue.java @@ -247,7 +247,7 @@ public class RomilyWeaklaxDialogue extends DialoguePlugin { @Override public boolean handle(NodeUsageEvent event) { if (!event.getPlayer().getAchievementDiaryManager().getDiary(DiaryType.VARROCK).isComplete(2,5)) { - event.getPlayer().getDialogueInterpreter().open(3205, event.getUsedItem()); + event.getPlayer().getDialogueInterpreter().open(3205, event.getUsedWith().asNpc(), event.getUsedItem()); } return true; } From 8d7f78ac5b73b08823efcfeeb2ba018912924c54 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:27:21 +0000 Subject: [PATCH 13/23] Implemented Varrock hard achievement for recovering Family Crest gauntlets --- .../misthalin/varrock/quest/familycrest/DimintheisDialogue.kt | 2 ++ .../main/core/game/node/entity/player/link/diary/DiaryType.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Server/src/main/content/region/misthalin/varrock/quest/familycrest/DimintheisDialogue.kt b/Server/src/main/content/region/misthalin/varrock/quest/familycrest/DimintheisDialogue.kt index e5e2f9ba0..fc857c7e6 100644 --- a/Server/src/main/content/region/misthalin/varrock/quest/familycrest/DimintheisDialogue.kt +++ b/Server/src/main/content/region/misthalin/varrock/quest/familycrest/DimintheisDialogue.kt @@ -7,6 +7,7 @@ import core.game.node.entity.player.Player import core.plugin.Initializable import org.rs09.consts.Items import content.data.Quests +import core.game.node.entity.player.link.diary.DiaryType @Initializable @@ -173,6 +174,7 @@ class DimintheisDialogue(player: Player? = null): core.game.dialogue.DialoguePlu 6000 -> npc("Not to worry, here they are").also { stage = 1000 addItem(player, getAttribute(player, "family-crest:gauntlets", Items.FAMILY_GAUNTLETS_778)) + player.achievementDiaryManager.finishTask(player, DiaryType.VARROCK, 2, 9) } 1000 -> end() diff --git a/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java b/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java index 8a8e51827..0a8e05b3e 100644 --- a/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java +++ b/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java @@ -140,7 +140,7 @@ public enum DiaryType { "Craft an air battlestaff", "Give your player-owned house a tropical wood or fancy stone

finish at the Varrock estate agent's", "Make a Varrock teleport tablet on a mahogany lectern", - "Obtain a new set of Family Crest gauntlets from Dimintheis", // TODO need family crest + "Obtain a new set of Family Crest gauntlets from Dimintheis", "Make a Waka Canoe near Edgeville", "Use the Home Teleport spell in the Ancient Magicks spellbook

to teleport to Edgeville", "Use the skull sceptre to teleport to Barbarian Village" From 0b41c5a14686b270d05322b03cae1503207091cd Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:28:30 +0000 Subject: [PATCH 14/23] Implemented Varrock hard achievement for getting a spottier cape Fixed an inauthentic typo in Matthias's falconry dialog --- .../handlers/iface/FurClothingInterface.kt | 5 + .../dialogue/MathiasFlaconryDialogue.java | 153 ++++++++++++++++-- 2 files changed, 149 insertions(+), 9 deletions(-) diff --git a/Server/src/main/content/global/handlers/iface/FurClothingInterface.kt b/Server/src/main/content/global/handlers/iface/FurClothingInterface.kt index e9025bad9..a335a5fbb 100644 --- a/Server/src/main/content/global/handlers/iface/FurClothingInterface.kt +++ b/Server/src/main/content/global/handlers/iface/FurClothingInterface.kt @@ -6,6 +6,7 @@ import core.game.component.ComponentDefinition import core.game.component.ComponentPlugin import core.game.container.access.InterfaceContainer import core.game.node.entity.player.Player +import core.game.node.entity.player.link.diary.DiaryType import core.game.node.item.Item import core.plugin.Initializable import core.plugin.Plugin @@ -165,6 +166,10 @@ class FurClothingInterface : ComponentPlugin(){ if (removeItem(player, requiredFur, Container.INVENTORY) && removeItem(player, coins, Container.INVENTORY)) { addItem(player, clothing.product.id, amount) + + if (clothing == FUR_CLOTHING.DASH_CAPE) { + player.achievementDiaryManager.finishTask(player, DiaryType.VARROCK, 2, 2) + } } } diff --git a/Server/src/main/content/region/kandarin/pisc/dialogue/MathiasFlaconryDialogue.java b/Server/src/main/content/region/kandarin/pisc/dialogue/MathiasFlaconryDialogue.java index dd3a27bd2..b926d7fde 100644 --- a/Server/src/main/content/region/kandarin/pisc/dialogue/MathiasFlaconryDialogue.java +++ b/Server/src/main/content/region/kandarin/pisc/dialogue/MathiasFlaconryDialogue.java @@ -8,6 +8,7 @@ import core.game.node.entity.npc.NPC; import core.game.node.entity.player.Player; import core.plugin.Initializable; import core.game.node.item.Item; +import org.rs09.consts.Items; /** * Handles the MathiasFlaconryDialogue dialogue. @@ -49,7 +50,8 @@ public class MathiasFlaconryDialogue extends DialoguePlugin { } break; case 323: - end(); + interpreter.sendDialogues(5093, FacialExpression.NEUTRAL, "Well, you're welcome to come back if you change", "your mind."); + stage = 967; break; case 95: if (player.getBank().containsItem(FALCON) || player.getEquipment().containsItem(FALCON) || player.getInventory().containsItem(FALCON)) { @@ -58,7 +60,7 @@ public class MathiasFlaconryDialogue extends DialoguePlugin { return true; } if (player.getEquipment().get(EquipmentContainer.SLOT_HANDS) != null || player.getEquipment().get(EquipmentContainer.SLOT_SHIELD) != null || player.getEquipment().get(EquipmentContainer.SLOT_WEAPON) != null) { - interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Sorry, free your hands, weapon, and shield slot first."); + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Sorry, you really need both hands free for falconry. I'd", "suggest that you put away your weapons and gloves before", "we start."); stage = 99; break; } @@ -69,11 +71,11 @@ public class MathiasFlaconryDialogue extends DialoguePlugin { stage = 97; } else { end(); - player.getPacketDispatch().sendMessage("You need 500 gold goins."); + player.getPacketDispatch().sendMessage("You need 500 gold coins."); } break; case 97: - interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Don't worry: I'll keep and eye on you to make sure", "you don't upset it roo much."); + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Don't worry; I'll keep an eye on you to make sure", "you don't upset it too much."); stage = 99; break; case 99: @@ -84,22 +86,109 @@ public class MathiasFlaconryDialogue extends DialoguePlugin { stage = 501; break; case 501: - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Could I have a go with your bird?"); + interpreter.sendOptions("Select an Option", "Do you have any quests I could do?", "What is this place?", "Could I have a go with your bird?"); stage = 502; break; case 502: + switch (buttonId) { + case 1: + interpreter.sendDialogues(player, FacialExpression.ASKING, "Do you have any quests I could do?"); + stage = 600; + break; + case 2: + interpreter.sendDialogues(player, FacialExpression.ASKING, "What is this place?"); + stage = 700; + break; + case 3: + interpreter.sendDialogues(player, FacialExpression.ASKING, "Could I have a go with your bird?"); + stage = 800; + break; + } + break; + case 600: + interpreter.sendDialogues(5093, FacialExpression.ASKING, "A quest? What a strange notion. Do you normally go", "around asking complete strangers for quests?"); + stage = 601; + break; + case 601: + interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Er, yes, now you come to mention it."); + stage = 602; + break; + case 602: + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Oh, ok then. Well, no, I don't; sorry."); + stage = 967; + break; + + case 700: + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "A good question; straight and to the point. My name is ", "Matthias, I am a falconer, and this is where I train", "my birds."); + stage = 701; + break; + case 701: + interpreter.sendOptions("Select an Option", "Do you have any quests I could do?", "That sounds like fun; could I have a go?", "That doesn't sound like my sort of thing.", "What's this falconry thing all about then?"); + stage = 702; + break; + case 702: + switch (buttonId) { + case 1: + interpreter.sendDialogues(player, FacialExpression.ASKING, "Do you have any quests I could do?"); + stage = 600; + break; + case 2: + interpreter.sendDialogues(player, FacialExpression.ASKING, "That sounds like fun; could I have a go?"); + stage = 800; + break; + case 3: + interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "That doesn't sound like my sort of thing."); + stage = 720; + break; + case 4: + interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "What's this falconry thing all about then?"); + stage = 750; + break; + } + break; + case 720: + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Fair enough; it does require a great deal of patience and", "skill, so I can understand if you might feel intimidated."); + stage = 967; + break; + case 750: + interpreter.sendDialogues(5093, FacialExpression.NEUTRAL, "Well, some people see it as a sport, although such a term", "does not really convey the amount of patience and", "dedication required to be proficient at the task."); + stage = 751; + break; + case 751: + interpreter.sendDialogues(5093, FacialExpression.NEUTRAL, "Putting it simply, it is the training and use of birds of", "prey in hunting quarry."); + stage = 752; + break; + case 752: + interpreter.sendDialogues(player, FacialExpression.ASKING, "So it's like keeping a pet then?"); + stage = 753; + break; + case 753: + interpreter.sendDialogues(5093, FacialExpression.NEUTRAL, "Not exactly, no. Such a bird can never really be", "considered tame in the same way that a dog can."); + stage = 754; + break; + case 754: + interpreter.sendDialogues(5093, FacialExpression.NEUTRAL, "They can be trained to associate people or places with", "food though, and, as such, a good falconer can get a", "trained bird to do as he wishes."); + stage = 701; + break; + + case 800: if (player.getSkills().getLevel(Skills.HUNTER) < 43) { npc("Try coming back when you're more experienced", "I wouldn't want my birds being injured."); stage = 967; return true; } - interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Training falcons is a lot of work and I", "doubt you're up to the task. However, I suppose", "I could let you try hunting with one."); - stage = 503; + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Training falcons is a lot of work and I doubt you're up", "to the task. However, I suppose I could let you try", "hunting with one."); + stage = 801; break; - case 503: - interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "I have some tamer birds that I occasionally lend to rich", "noblemen who consider it a sufficiently refined sport for", "their tastes. and you look like the kind who might", "appreciate a good hunt."); + case 801: + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "I have some tamer birds that I occasionally lend to rich", "noblemen who consider it a sufficiently refined sport for", "their tastes, and you look like the kind who might", "appreciate a good hunt."); + stage = 802; + break; + case 802: + interpreter.sendDialogues(5093, FacialExpression.NEUTRAL, "I'd have to request a small fee, mind you; how does", "500 gold pieces sound?"); stage = 90; break; + case 900: interpreter.sendOptions("Select an Option", "Yes, please.", "No thank you."); stage = 901; @@ -125,7 +214,48 @@ public class MathiasFlaconryDialogue extends DialoguePlugin { case 967: end(); break; + + case 1000: + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Ah, you're back. How are you getting along with her then?"); + stage = 1001; + break; + case 1001: + interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "It's certainly harder than it looks."); + stage = 1002; + break; + case 1002: + interpreter.sendDialogues(5093, FacialExpression.HALF_GUILTY, "Sorry, but I was talking to the falcon, not you. But yes it", "is. Have you had enough yet?"); + stage = 1003; + break; + case 1003: + interpreter.sendOptions("Select an Option", "Actually, I'd like to keep trying a little longer.", "I think I'll leave it for now."); + stage = 1004; + break; + case 1004: + switch (buttonId) { + case 1: + interpreter.sendDialogues(player, FacialExpression.ASKING, "Actually, I'd like to keep trying a little longer."); + stage = 1010; + break; + case 2: + interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "I think I'll leave it for now."); + stage = 1020; + break; + } + break; + case 1010: + interpreter.sendDialogues(5093, FacialExpression.NEUTRAL, "Ok then, just come talk to me when you're done."); + stage = 967; + break; + case 1020: + player.getInventory().remove(FALCON); + player.getEquipment().remove(FALCON, true); + + interpreter.sendDialogue("You give the falcon and glove back to Matthias."); + stage = 967; + break; } + return true; } @@ -145,6 +275,11 @@ public class MathiasFlaconryDialogue extends DialoguePlugin { stage = 900; return true; } + if (player.getEquipment().contains(10024, 1) || player.getInventory().contains(10024, 1)) { + interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, "Hello again."); + stage = 1000; + return true; + } if (args.length == 2) quick = true; if (quick) { From 501b75aa2236d5ac03d70c48c09c37f9dc1f079d Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:28:56 +0000 Subject: [PATCH 15/23] Implemented Varrock medium achievement for using the digsite pendant --- Server/src/main/content/data/EnchantedJewellery.kt | 10 +++++++--- .../game/node/entity/player/link/diary/DiaryType.java | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Server/src/main/content/data/EnchantedJewellery.kt b/Server/src/main/content/data/EnchantedJewellery.kt index a0c53ad00..61373e8f3 100644 --- a/Server/src/main/content/data/EnchantedJewellery.kt +++ b/Server/src/main/content/data/EnchantedJewellery.kt @@ -9,15 +9,14 @@ import core.game.event.TeleportEvent import core.game.interaction.QueueStrength import core.game.node.entity.player.Player import core.game.node.entity.player.link.TeleportManager +import core.game.node.entity.player.link.diary.DiaryType import core.game.node.item.Item -import core.game.system.task.Pulse import core.game.world.GameWorld import core.game.world.map.Location import core.game.world.update.flag.context.Animation import core.game.world.update.flag.context.Graphics -import org.rs09.consts.Items -import core.game.world.GameWorld.Pulser import core.tools.Log +import org.rs09.consts.Items import org.rs09.consts.Sounds import java.util.* @@ -258,6 +257,11 @@ enum class EnchantedJewellery( resetAnimator(player) unlock(player) player.dispatch(TeleportEvent(TeleportManager.TeleportType.NORMAL, TeleportMethod.JEWELRY, item, location)) + + if (DIGSITE_PENDANT.ids.contains(item.id)) { + player.achievementDiaryManager.finishTask(player, DiaryType.VARROCK, 1, 10) + } + if (!replace) { return@queueScript stopExecuting(player) } diff --git a/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java b/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java index 0a8e05b3e..dbbb06e81 100644 --- a/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java +++ b/Server/src/main/core/game/node/entity/player/link/diary/DiaryType.java @@ -1,8 +1,8 @@ package core.game.node.entity.player.link.diary; import core.game.node.entity.player.Player; -import org.rs09.consts.Items; import core.game.node.item.Item; +import org.rs09.consts.Items; /** * An achievement diary type. @@ -118,7 +118,7 @@ public enum DiaryType { "Select a colour for a new kitten", // TODO need ring of charos(a) and garden of tranquility to start "Use the shortcut under the wall, north-west of the Grand

Exchange", "Enter the A Soul's Bane rift", - "Teleport to the Digsite using a Digsite pendant", // TODO need Digsite and museum + "Teleport to the Digsite using a Digsite pendant", "Craft an earth tiara on the Earth Altar", "Pickpocket a guard in the Varrock Palace courtyard", "Use the teleport to Varrock spell", From 50eb7582dda99e7d2646fe2f26cddd9a352cc84a Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:29:50 +0000 Subject: [PATCH 16/23] Fixed a bug where depositing the full inventory doesn't work when the bank is full and there's noted items in the player's inventory --- .../game/container/impl/BankContainer.java | 25 ++++++++----------- Server/src/main/core/game/node/item/Item.java | 13 ++++++++++ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/Server/src/main/core/game/container/impl/BankContainer.java b/Server/src/main/core/game/container/impl/BankContainer.java index 5f8a51215..b27ecf700 100644 --- a/Server/src/main/core/game/container/impl/BankContainer.java +++ b/Server/src/main/core/game/container/impl/BankContainer.java @@ -1,13 +1,10 @@ package core.game.container.impl; -import core.api.IfaceSettingsBuilder; -import core.game.container.access.InterfaceContainer; -import kotlin.Unit; -import kotlin.ranges.IntRange; -import org.rs09.consts.Vars; import core.ServerConstants; +import core.api.IfaceSettingsBuilder; import core.game.component.Component; import core.game.container.*; +import core.game.container.access.InterfaceContainer; import core.game.node.entity.player.Player; import core.game.node.entity.player.link.IronmanMode; import core.game.node.item.Item; @@ -16,8 +13,8 @@ import core.game.world.GameWorld; import core.net.packet.PacketRepository; import core.net.packet.context.ContainerContext; import core.net.packet.out.ContainerPacket; - -import java.nio.ByteBuffer; +import kotlin.ranges.IntRange; +import org.rs09.consts.Vars; import static core.api.ContentAPIKt.*; @@ -201,14 +198,9 @@ public final class BankContainer extends Container { } item = new Item(item.getId(), amount, item.getCharge()); - boolean unnote = !item.getDefinition().isUnnoted(); + Item add = item.toUnnotedItem(); - Item add = unnote ? new Item(item.getDefinition().getNoteId(), amount, item.getCharge()) : item; - if (unnote && !add.getDefinition().isUnnoted()) { - add = item; - } - - int maxCount = super.getMaximumAdd(add); + int maxCount = getMaximumAdd(add); if (amount > maxCount) { add.setAmount(maxCount); item.setAmount(maxCount); @@ -474,6 +466,11 @@ public final class BankContainer extends Container { return open; } + @Override + public int getMaximumAdd(Item item) { + return super.getMaximumAdd(item.toUnnotedItem()); + } + /** * Listens to the bank container. * @author Emperor diff --git a/Server/src/main/core/game/node/item/Item.java b/Server/src/main/core/game/node/item/Item.java index 086784cf6..c95077106 100644 --- a/Server/src/main/core/game/node/item/Item.java +++ b/Server/src/main/core/game/node/item/Item.java @@ -135,6 +135,19 @@ public class Item extends Node{ return getId(); } + /** + * Converts noted items into unnoted items. + * + * @return The unnoted version of the item. Returns the original Item if already unnoted. + */ + public Item toUnnotedItem() { + if (definition.isUnnoted()) { + return this; + } else { + return new Item(definition.getNoteId(), getAmount(), getCharge()); + } + } + /** * @return the id */ From f98495d1aecab92ded2f180f91c92aa01206da8a Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:31:07 +0000 Subject: [PATCH 17/23] Made it possible to kill the Lesser Demon Champion --- .../activity/cchallange/npc/LesserDemonChampionNPC.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Server/src/main/content/global/activity/cchallange/npc/LesserDemonChampionNPC.kt b/Server/src/main/content/global/activity/cchallange/npc/LesserDemonChampionNPC.kt index e62f73bf2..e41fd5d72 100644 --- a/Server/src/main/content/global/activity/cchallange/npc/LesserDemonChampionNPC.kt +++ b/Server/src/main/content/global/activity/cchallange/npc/LesserDemonChampionNPC.kt @@ -1,8 +1,6 @@ package content.global.activity.cchallange.npc import core.api.* -import core.game.container.impl.EquipmentContainer -import core.game.global.action.EquipHandler import core.game.node.entity.Entity import core.game.node.entity.combat.BattleState import core.game.node.entity.npc.AbstractNPC @@ -62,12 +60,11 @@ class LesserDemonChampionNPC(id: Int = 0, location: Location? = null) : Abstract super.checkImpact(state) val player = state.attacker if (player is Player) { - if (!player.equipment[3].hasItemPlugin()) { + if (player.equipment.isEmpty) { state.neutralizeHits() state.estimatedHit = state.maximumHit } else { - EquipHandler.unequip(player, EquipmentContainer.SLOT_WEAPON, id) - sendMessage(player, "You cannot use weapons in this challenge.") + sendMessage(player, "You cannot wear any equipment in this challenge.") if (state.estimatedHit > -1) { state.estimatedHit = 0 return From cf6cc08b26ea8b002758aa3737414fd6d8a04d54 Mon Sep 17 00:00:00 2001 From: Sam Marder Date: Sat, 30 May 2026 14:31:23 +0000 Subject: [PATCH 18/23] Made it possible to kill the Zombie Champion --- .../content/global/activity/cchallange/npc/ZombieChampionNPC.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main/content/global/activity/cchallange/npc/ZombieChampionNPC.kt b/Server/src/main/content/global/activity/cchallange/npc/ZombieChampionNPC.kt index 0be9eeb3d..bab84b5b5 100644 --- a/Server/src/main/content/global/activity/cchallange/npc/ZombieChampionNPC.kt +++ b/Server/src/main/content/global/activity/cchallange/npc/ZombieChampionNPC.kt @@ -63,8 +63,8 @@ class ZombieChampionNPC(id: Int = 0, location: Location? = null) : AbstractNPC(i val player = state.attacker if (player is Player) { if (state.style == CombatStyle.MELEE || state.style == CombatStyle.RANGE) { - state.estimatedHit = state.maximumHit state.neutralizeHits() + state.estimatedHit = state.maximumHit } if (state.style == CombatStyle.MAGIC) { sendMessage(player, "You cannot use spells in this challenge.") From 130cc79a171276be96aa26746b8fba1ce64e85b2 Mon Sep 17 00:00:00 2001 From: oftheshire Date: Sat, 30 May 2026 14:37:24 +0000 Subject: [PATCH 19/23] Clivet now reappears in Hazeel Cult --- .../kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt index 84474795a..c33424281 100644 --- a/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt +++ b/Server/src/main/content/region/kandarin/ardougne/quest/hazeelcult/ClivetNPC.kt @@ -1,6 +1,5 @@ package content.region.kandarin.ardougne.quest.hazeelcult -import core.api.* import core.game.node.entity.npc.AbstractNPC import core.game.world.map.Location import core.plugin.Initializable @@ -16,16 +15,18 @@ class ClivetNPC(id: Int = 0, location: Location? = null, ) : AbstractNPC(id, loc override fun getIds(): IntArray = intArrayOf(NPCs.CLIVET_893) - private var invisibilityTimerRunning = false + private var invisTicks = 0 override fun tick() { + if (isInvisible && invisTicks <= 0) { + invisTicks = 20 + } - if (isInvisible && !invisibilityTimerRunning) { - invisibilityTimerRunning = true + if (invisTicks > 0) { + invisTicks-- - runTask(this, 20) { + if (invisTicks == 0) { isInvisible = false - invisibilityTimerRunning = false } } From bc6fd94f46054ada724caff8abdba714e0b319b9 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Sat, 30 May 2026 17:38:42 +0300 Subject: [PATCH 20/23] =?UTF-8?q?Added=20chat=20support=20for=20=C3=A4/?= =?UTF-8?q?=C3=B6/=C3=A5=20characters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game/world/update/flag/PlayerFlags530.kt | 8 ++--- .../net/packet/out/CommunicationMessage.java | 16 ++++++---- Server/src/main/core/tools/CP1252.java | 32 +++++++++++++++++-- Server/src/main/core/tools/StringUtils.java | 20 ++++++------ 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/Server/src/main/core/game/world/update/flag/PlayerFlags530.kt b/Server/src/main/core/game/world/update/flag/PlayerFlags530.kt index bcc1e3356..a83f0235c 100644 --- a/Server/src/main/core/game/world/update/flag/PlayerFlags530.kt +++ b/Server/src/main/core/game/world/update/flag/PlayerFlags530.kt @@ -11,7 +11,6 @@ import core.tools.* import core.api.* import core.game.world.GameWorld -import java.nio.charset.StandardCharsets import kotlin.reflect.* import kotlin.math.max @@ -28,12 +27,13 @@ sealed class PlayerFlags530 (p: Int, o: Int, f: EntityFlag) : EFlagProvider (530 else buffer.p1 (context.chatIcon) val chatBuf = ByteArray(256) - chatBuf[0] = context.text.length.toByte() + val encodedText = CP1252.toBytes(context.text) + chatBuf[0] = encodedText.size.toByte() val offset = 1 + StringUtils.encryptPlayerChat ( chatBuf, 0, 1, - context.text.length, - context.text.toByteArray(StandardCharsets.UTF_8) + encodedText.size, + encodedText ) buffer.p1 (offset + 1) buffer.putReverse (chatBuf, 0, offset) diff --git a/Server/src/main/core/net/packet/out/CommunicationMessage.java b/Server/src/main/core/net/packet/out/CommunicationMessage.java index 5cd5e592d..77e86af95 100644 --- a/Server/src/main/core/net/packet/out/CommunicationMessage.java +++ b/Server/src/main/core/net/packet/out/CommunicationMessage.java @@ -6,6 +6,7 @@ import core.net.packet.IoBuffer; import core.net.packet.OutgoingPacket; import core.net.packet.PacketHeader; import core.net.packet.context.MessageContext; +import core.tools.CP1252; import core.tools.StringUtils; import core.game.bots.AIPlayer; @@ -21,20 +22,21 @@ public final class CommunicationMessage implements OutgoingPacket= 128 && out < 160) { - int cp1252 = charMap[out - 128]; + int cp1252 = CHAR_MAP[out - 128]; if (cp1252 == 0) { cp1252 = 63; } @@ -20,4 +24,26 @@ public class CP1252 { } return (char) out; } + + public static byte getByte(char value) { + if ((value > 0 && value < 128) || (value >= 160 && value <= 255)) { + return (byte) value; + } + + for (int i = 0; i < CHAR_MAP.length; i++) { + if (CHAR_MAP[i] != 0 && CHAR_MAP[i] == value) { + return (byte) (i + 128); + } + } + + return 63; + } + + public static byte[] toBytes(CharSequence value) { + byte[] out = new byte[value.length()]; + for (int i = 0; i < value.length(); i++) { + out[i] = getByte(value.charAt(i)); + } + return out; + } } diff --git a/Server/src/main/core/tools/StringUtils.java b/Server/src/main/core/tools/StringUtils.java index 84ac0e3e0..bc55800b6 100644 --- a/Server/src/main/core/tools/StringUtils.java +++ b/Server/src/main/core/tools/StringUtils.java @@ -502,7 +502,7 @@ public final class StringUtils { return ""; int charsDecoded = 0; int i_4_ = 0; - String s = ""; + StringBuilder s = new StringBuilder(totalChars); for (;;) { byte i_7_ = (byte) buffer.get(); if (i_7_ >= 0) @@ -511,7 +511,7 @@ public final class StringUtils { i_4_ = anIntArray241[i_4_]; int i_8_; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (totalChars <= ++charsDecoded) break; i_4_ = 0; @@ -521,7 +521,7 @@ public final class StringUtils { else i_4_++; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (++charsDecoded >= totalChars) break; i_4_ = 0; @@ -531,7 +531,7 @@ public final class StringUtils { else i_4_ = anIntArray241[i_4_]; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (totalChars <= ++charsDecoded) break; i_4_ = 0; @@ -541,7 +541,7 @@ public final class StringUtils { else i_4_ = anIntArray241[i_4_]; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (totalChars <= ++charsDecoded) break; @@ -552,7 +552,7 @@ public final class StringUtils { else i_4_++; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (++charsDecoded >= totalChars) break; i_4_ = 0; @@ -562,7 +562,7 @@ public final class StringUtils { else i_4_ = anIntArray241[i_4_]; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (totalChars <= ++charsDecoded) break; i_4_ = 0; @@ -572,7 +572,7 @@ public final class StringUtils { else i_4_++; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (totalChars <= ++charsDecoded) break; i_4_ = 0; @@ -582,13 +582,13 @@ public final class StringUtils { else i_4_++; if ((i_8_ = anIntArray241[i_4_]) < 0) { - s += (char) (byte) (i_8_ ^ 0xffffffff); + s.append(CP1252.getFromByte((byte) (i_8_ ^ 0xffffffff))); if (++charsDecoded >= totalChars) break; i_4_ = 0; } } - return s; + return s.toString(); } catch (RuntimeException runtimeexception) { runtimeexception.printStackTrace(); } From fc031be6ff05e327cfd402608eb2155ea5d1cc43 Mon Sep 17 00:00:00 2001 From: Beck <2421110-beckrickert@users.noreply.gitlab.com> Date: Sat, 30 May 2026 14:39:49 +0000 Subject: [PATCH 21/23] Fixed quest journal typos in Prince Ali Rescue --- .../princealirescue/PrinceAliRescue.java | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Server/src/main/content/region/desert/alkharid/quest/princealirescue/PrinceAliRescue.java b/Server/src/main/content/region/desert/alkharid/quest/princealirescue/PrinceAliRescue.java index 049e805f6..1c37aebab 100644 --- a/Server/src/main/content/region/desert/alkharid/quest/princealirescue/PrinceAliRescue.java +++ b/Server/src/main/content/region/desert/alkharid/quest/princealirescue/PrinceAliRescue.java @@ -64,16 +64,16 @@ public class PrinceAliRescue extends Quest { break; case 10: line(player, "I started this quest by speaking to Hassan in Al-Kharid", 4+ 7); - line(player, "Palace. he told me I should speak to Osman the spymaster.", 5+ 7); + line(player, "Palace. He told me I should speak to Osman the spymaster.", 5+ 7); line(player, BLUE + "I should go and speak to " + RED + "Osman " + BLUE + "for details on the quest.", 6+ 7); break; case 20: line(player, "I started this quest by speaking to Hassan in Al-Kharid", 4+ 7); - line(player, "Palace. he told me I should speak to Osman the spymaster.", 5+ 7); + line(player, "Palace. He told me I should speak to Osman the spymaster.", 5+ 7); line(player, "I should go and speak to Osman for details on the quest.", 6+ 7); line(player, RED + "Prince Ali " + BLUE + "has been " + RED + "kidnapped " + BLUE + "but luckily the spy " + RED + "Leela " + BLUE + "has", 7+ 7); - line(player, BLUE + "found he is being held near " + RED + "Draynor village. " + BLUE + "I will need to", 8+ 7); - line(player, RED + "disguise " + BLUE + "the " + RED + "Price " + BLUE + "and " + RED + "tie " + BLUE + "up his " + RED + "captor " + BLUE + "to " + RED + "free " + BLUE + "him from", 9+ 7); + line(player, BLUE + "found he is being held near " + RED + "draynor village. " + BLUE + "I will need to", 8+ 7); //authentic typo on the 'draynor village' capitialization + line(player, RED + "disguise " + BLUE + "the " + RED + "Prince " + BLUE + "and " + RED + "tie " + BLUE + "up his " + RED + "captor " + BLUE + "to " + RED + "free " + BLUE + "him from", 9+ 7); line(player, BLUE + "their " + RED + "clutches.", 10+ 7); line(player, BLUE + "To do this I should:-", 11+ 7); line(player, BLUE + "Talk to " + RED + "Leela " + BLUE + "near " + RED + "Draynor Village " + BLUE + "for advice.", 12+ 7); @@ -85,10 +85,10 @@ public class PrinceAliRescue extends Quest { break; case 30: line(player, "I started this quest by speaking to Hassan in Al-Kharid", 4+ 7); - line(player, "Palace. he told me I should speak to Osman the spymaster.", 5+ 7); + line(player, "Palace. He told me I should speak to Osman the spymaster.", 5+ 7); line(player, "I should go and speak to Osman for details on the quest.", 6+ 7); line(player, RED + "Prince Ali " + BLUE + "has been " + RED + "kidnapped " + BLUE + "but luckily the spy " + RED + "Leela " + BLUE + "has", 7+ 7); - line(player, BLUE + "found he is being held near " + RED + "Draynor village. " + BLUE + "I will need to", 8+ 7); + line(player, BLUE + "found he is being held near " + RED + "Draynor Village. " + BLUE + "I will need to", 8+ 7); line(player, RED + "disguise " + BLUE + "the " + RED + "Prince " + BLUE + "and " + RED + "tie " + BLUE + "up his " + RED + "captor " + BLUE + "to " + RED + "free " + BLUE + "him from", 9+ 7); line(player, BLUE + "their " + RED + "clutches.", 10+ 7); line(player, BLUE + "To do this I should:-", 11+ 7); @@ -97,15 +97,15 @@ public class PrinceAliRescue extends Quest { line(player, hasItem(player, ROPE) ? "I have some rope with me." : BLUE + "Get some " + RED + "rope " + BLUE + "to tie up the Princes' " + RED + "kidnapper.", 14+ 7); line(player, hasItem(player, PASTE) ? "I have some skin paste suitable for disguise with me." : BLUE + "Get something to " + RED + "colour " + BLUE + "the " + RED + "Princes' skin " + BLUE + "as a " + RED + "disguise.", 15+ 7); line(player, hasItem(player, SKIRT) ? "I have a skirt suitable for a disguise with me." : BLUE + "Get a " + RED + "skirt " + BLUE + "similar to his " + RED + "kidnapper " + BLUE + "as " + RED + "disguise.", 16+ 7); - line(player, hasItem(player, YELLOW_WIG) ? "I have a wig suitable for disguise with me." : BLUE + "Get a " + RED + "Wig " + BLUE + "to " + RED + "help disguise" + BLUE + "the " + RED + "prince.", 17+ 7); + line(player, hasItem(player, YELLOW_WIG) ? "I have a wig suitable for disguise with me." : BLUE + "Get a " + RED + "Wig " + BLUE + "to " + RED + "help disguise " + BLUE + "the " + RED + "prince.", 17+ 7); break; case 40: line(player, "I started this quest by speaking to Hassan in Al-Kharid", 4+ 7); - line(player, "Palace. he told me I should speak to Osman the spymaster.", 5+ 7); + line(player, "Palace. He told me I should speak to Osman the spymaster.", 5+ 7); line(player, "I should go and speak to Osman for details on the quest.", 6+ 7); if (player.getAttribute("guard-drunk", false)) { line(player, "Prince Ali has been kidnapped but luckily the spy Leela has", 7+ 7); - line(player, "found he is being held near Draynor village. I will need to", 8+ 7); + line(player, "found he is being held near Draynor Village. I will need to", 8+ 7); line(player, "disguise the Prince and tie up his captor to free him from", 9+ 7); line(player, "their clutches.", 10+ 7); line(player, "I also had to prevent the Guard from seeing that I was up", 10+ 7); @@ -116,32 +116,32 @@ public class PrinceAliRescue extends Quest { } else { line(player, BLUE + "Do something to prevent " + RED + "Joe the Guard " + BLUE + "seeing the", 7+ 7); line(player, BLUE + "escape.", 8+ 7); - line(player, BLUE + "Use the " + RED + "Skin potion" + BLUE + ", " + RED + "Pink Skirt" + BLUE + "," + RED + "Rope" + BLUE + "," + RED + "Blonde Wig " + BLUE + "and " + RED + "Cell", 9+ 7); + line(player, BLUE + "Use the " + RED + "Skin Potion" + BLUE + ", " + RED + "Pink Skirt" + BLUE + ", " + RED + "Rope" + BLUE + ", " + RED + "Blonde Wig " + BLUE + "and " + RED + "Cell", 9+ 7); line(player, RED + "Key" + BLUE + " to free " + RED + "Prince Ali " + BLUE + "from his cell somehow.", 10+ 7); } break; case 50: line(player, "I started this quest by speaking to Hassan in Al-Kharid", 4+ 7); - line(player, "Palace. he told me I should speak to Osman the spymaster.", 5+ 7); + line(player, "Palace. He told me I should speak to Osman the spymaster.", 5+ 7); line(player, "I should go and speak to Osman for details on the quest.", 6+ 7); line(player, "Prince Ali has been kidnapped but luckily the spy Leela has", 7+ 7); - line(player, "found he is being held near Draynor village. I will need to", 8+ 7); + line(player, "found he is being held near Draynor Village. I will need to", 8+ 7); line(player, "disguise the Prince and tie up his captor to free him from", 9+ 7); line(player, "their clutches.", 10+ 7); line(player, "I also had to prevent the Guard from seeing that I was up", 10+ 7); line(player, "to, by getting him drunk.", 11+ 7); line(player, "With the guard disposed of, I used my rope to tie up Lady", 11+ 7); line(player, "Keli in a cupboard, so I could disguise the Prince.", 12+ 7); - line(player, BLUE + "I need to " + RED + "Unlock the cell door " + BLUE + "and then give the Prince the", 13+ 7); - line(player, RED + "Pink Skirt" + BLUE + ", the " + RED + "Skin paste " + BLUE + "and the " + RED + "Blonde Swig " + BLUE + "so that the", 14+ 7); + line(player, BLUE + "I need to " + RED + "unlock the cell door " + BLUE + "and then give the Prince the", 13+ 7); + line(player, RED + "Pink Skirt" + BLUE + ", the " + RED + "Skin Paste " + BLUE + "and the " + RED + "Blonde Wig " + BLUE + "so that they", 14+ 7); line(player, BLUE + "can safely " + RED + "escape " + BLUE + "disguised as " + RED + "Lady Keli.", 15+ 7); break; case 60: line(player, "I started this quest by speaking to Hassan in Al-Kharid", 4+ 7); - line(player, "Palace. he told me I should speak to Osman the spymaster.", 5+ 7); + line(player, "Palace. He told me I should speak to Osman the spymaster.", 5+ 7); line(player, "I should go and speak to Osman for details on the quest.", 6+ 7); line(player, "Prince Ali has been kidnapped but luckily the spy Leela has", 7+ 7); - line(player, "found he is being held near Draynor village. I will need to", 8+ 7); + line(player, "found he is being held near Draynor Village. I will need to", 8+ 7); line(player, "disguise the Prince and tie up his captor to free him from", 9+ 7); line(player, "their clutches.", 10+ 7); line(player, "I also had to prevent the Guard from seeing that I was up", 10+ 7); @@ -155,10 +155,10 @@ public class PrinceAliRescue extends Quest { break; case 100: line(player, "I started this quest by speaking to Hassan in Al-Kharid", 4+ 7); - line(player, "Palace. he told me I should speak to Osman the spymaster.", 5+ 7); + line(player, "Palace. He told me I should speak to Osman the spymaster.", 5+ 7); line(player, "I should go and speak to Osman for details on the quest.", 6+ 7); line(player, "Prince Ali has been kidnapped but luckily the spy Leela has", 7+ 7); - line(player, "found he is being held near Draynor village. I will need to", 8+ 7); + line(player, "found he is being held near Draynor Village. I will need to", 8+ 7); line(player, "disguise the Prince and tie up his captor to free him from", 9+ 7); line(player, "their clutches.", 10+ 7); line(player, "I also had to prevent the Guard from seeing that I was up", 10+ 7); @@ -170,7 +170,7 @@ public class PrinceAliRescue extends Quest { line(player, "freedom with Leela after unlocking his cell door.", 15+ 7); line(player, "Hassan the chancellor rewarded me for all of my help.", 16+ 7); line(player, "I am now a friend of Al-Kharid and may pass through the", 17+ 7); - line(player, "gate leading between Lumbridge and Al-Kharid for free", 18+ 7); + line(player, "gate leading between Lumbridge and Al-Kharid for free.", 18+ 7); line(player, "QUEST COMPLETE!", 19+ 7); break; } From da41694dd482af525c14dbd3153f0be4dc41ed66 Mon Sep 17 00:00:00 2001 From: DeadlyGenga <19836947-matthewhurleychch@users.noreply.gitlab.com> Date: Sat, 30 May 2026 14:42:06 +0000 Subject: [PATCH 22/23] Added an additional 37 spawn locations for penguins Corrected Entrana Penguin to a Barrel Corrected Observatory Penguin to Crate Implemented admin command to reset full penguins ::renewpenguins Implemented admin command to spawn individual penguin ::spawnpenguin --- .../activity/penguinhns/PenguinManager.kt | 2 +- .../activity/penguinhns/PenguinSpawner.kt | 57 ++++++++++++++-- .../command/sets/DevelopmentCommandSet.kt | 68 ++++++++++++++++++- 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/Server/src/main/content/global/activity/penguinhns/PenguinManager.kt b/Server/src/main/content/global/activity/penguinhns/PenguinManager.kt index c284214a7..48cdc7dec 100644 --- a/Server/src/main/content/global/activity/penguinhns/PenguinManager.kt +++ b/Server/src/main/content/global/activity/penguinhns/PenguinManager.kt @@ -30,7 +30,7 @@ class PenguinManager{ return tagMapping[Penguin.forLocation(location)?.ordinal]?.contains(player.username.toLowerCase()) ?: false } - private fun updateStoreFile(){ + fun updateStoreFile(){ val jsonTags = JSONArray() tagMapping.filter { it.value.isNotEmpty() }.forEach { (ordinal,taggers) -> log(this::class.java, Log.FINE, "$ordinal - ${taggers.first()}") diff --git a/Server/src/main/content/global/activity/penguinhns/PenguinSpawner.kt b/Server/src/main/content/global/activity/penguinhns/PenguinSpawner.kt index d19b9a82a..2d6abe7bb 100644 --- a/Server/src/main/content/global/activity/penguinhns/PenguinSpawner.kt +++ b/Server/src/main/content/global/activity/penguinhns/PenguinSpawner.kt @@ -44,18 +44,65 @@ enum class Penguin(val id: Int, val hint: String, val location: Location){ BUSH_8(NPCs.BUSH_8105,"located in the kingdom of Asgarnia.",Location.create(2951, 3511, 0)), BUSH_9(NPCs.BUSH_8105,"located in the northern desert.",Location.create(3350, 3311, 0)), BUSH_10(NPCs.BUSH_8105,"located somewhere in the kingdom of Kandarin.",Location.create(2633, 3501, 0)), - BUSH_11(NPCs.BUSH_8105,"located south of Ardougne.",Location.create(2440, 3206, 0)), - BUSH_12(NPCs.BUSH_8105,"located where wizards study.",Location.create(3112, 3149, 0)), + BUSH_11(NPCs.BUSH_8105,"located where wizards study.",Location.create(3112, 3149, 0)), ROCK_1(NPCs.ROCK_8109,"located where the Imperial Guard train.",Location.create(2852, 3578, 0)), ROCK_2(NPCs.ROCK_8109,"located in the kingdom of Misthalin.",Location.create(3356, 3416, 0)), - ROCK_3(NPCs.ROCK_8109,"located near some ogres.",Location.create(2631, 2980, 0)), + ROCK_3(NPCs.ROCK_8109,"located near some ogres.",Location.create(2631, 2980, 0)), //potentially incorrect location ROCK_4(NPCs.ROCK_8109,"located in the Kingdom of Asgarnia.",Location.create(3013, 3501, 0)), ROCK_5(NPCs.ROCK_8109,"located between Fremennik and barbarians.",Location.create(2532, 3630, 0)), CRATE_1(NPCs.CRATE_8108,"located in the kingdom of Misthalin.",Location.create(3112, 3332, 0)), CRATE_2(NPCs.CRATE_8108,"located in the Kingdom of Misthalin.",Location.create(3305, 3508, 0)), - BARREL_1(NPCs.CRATE_8108,"located where no weapons may go.",Location.create(2806, 3383, 0)), + CRATE_3(NPCs.CRATE_8108,"located south of Ardougne.",Location.create(2440, 3206, 0)), + BARREL_1(NPCs.BARREL_8104,"located where no weapons may go.",Location.create(2806, 3383, 0)), TOADSTOOL_1(NPCs.TOADSTOOL_8110,"located in the kingdom of Misthalin.",Location.create(3156, 3178, 0)), - TOADSTOOL_2(NPCs.TOADSTOOL_8110,"located in the fairy realm.",Location.create(2409, 4462, 0)); + TOADSTOOL_2(NPCs.TOADSTOOL_8110,"located in the fairy realm.",Location.create(2409, 4462, 0)), //potentially a spawn from 2010 + + BUSH_12(NPCs.BUSH_8105,"located on an island.",Location.create(2534, 3871, 0)), + BARREL_2(NPCs.BARREL_8104,"located near the ghost town.",Location.create(3654, 3491, 0)), + BUSH_13(NPCs.BUSH_8105,"located where bloodsuckers rule.",Location.create(3600, 3487, 0)), + BUSH_14(NPCs.BUSH_8105,"located on islands where brothers quarrel.",Location.create(2355, 3848, 0)), + ROCK_6(NPCs.ROCK_8109,"located on a large crescent island.",Location.create(2118, 3943, 0)), + ROCK_7(NPCs.ROCK_8109,"located on islands where brothers quarrel.",Location.create(2357, 3797, 0)), + ROCK_8(NPCs.ROCK_8109,"located in the Wilderness.",Location.create(3169, 3650, 0)), + BARREL_3(NPCs.BARREL_8104,"located where pirates feel mostly harmless.",Location.create(3738, 3001, 0)), + CACTUS_3(NPCs.CACTUS_8107,"located in the southern desert.",Location.create(3276, 2797, 0)), + TOADSTOOL_3(NPCs.TOADSTOOL_8110,"located near the pointy-eared ones.",Location.create(2314, 3174, 0)), + BUSH_15(NPCs.BUSH_8105,"located deep in the jungle.",Location.create(2938, 2978, 0)), + TOADSTOOL_4(NPCs.TOADSTOOL_8110,"located near the pointy-eared ones.",Location.create(2219, 3227, 0)), + BARREL_4(NPCs.BARREL_8104,"located south of Ardougne.",Location.create(2662, 3152, 0)), + //BARREL_5(NPCs.BARREL_8104,"located where monkeys rule.",Location.create(2751, 2700, 0)), + //currently not well accessible, will need to add when Ape Atoll is sorted. + //BUSH_16(NPCs.BUSH_8105,"located on islands where brothers quarrel.",Location.create(2353, 3834, 0)), + //currently not well accessible, will need to add when Neitiznot bridges are fixed. + ROCK_9(NPCs.ROCK_8109,"located near a mountain of wolves.",Location.create(2852, 3504, 0)), + ROCK_10(NPCs.ROCK_8109,"located on islands where brothers quarrel.",Location.create(2413, 3846, 0)), + + BUSH_17(NPCs.BUSH_8105,"located near some ogres.",Location.create(2578, 2909, 0)), + CRATE_4(NPCs.CRATE_8108,"located where banana smugglers dwell.",Location.create(2869, 3157, 0)), + //CRATE_5(NPCs.CRATE_8108,"located near the island of Dragontooth.",Location.create(3824, 3562, 0)), + //current not accessible, will need to add when Dragontooth island is sorted. + ROCK_11(NPCs.ROCK_8109,"located where bloodsuckers rule.",Location.create(3550, 3439, 0)), + TOADSTOOL_5(NPCs.TOADSTOOL_8110,"located near the pointy-eared ones.",Location.create(2181, 3172, 0)), + BUSH_18(NPCs.BUSH_8105,"located where bloodsuckers rule.",Location.create(3472, 3392, 0)), + BUSH_19(NPCs.BUSH_8105,"located near Port Sarim.",Location.create(2989, 3121, 0)), + CRATE_6(NPCs.CRATE_8108,"located where fishers colonise.",Location.create(2322, 3658, 0)), + ROCK_12(NPCs.ROCK_8109,"located between Fremennik and barbarians.",Location.create(2675, 3717, 0)), + ROCK_13(NPCs.ROCK_8109,"located near the pointy-eared ones.",Location.create(2296, 3270, 0)), + CRATE_7(NPCs.CRATE_8108,"located where bloodsuckers rule.",Location.create(3637, 3486, 0)), + TOADSTOOL_6(NPCs.TOADSTOOL_8110,"located where bloodsuckers rule.",Location.create(3416, 3437, 0)), + BUSH_20(NPCs.BUSH_8105,"located where monkeys rule.",Location.create(2802, 2806, 0)), + + ROCK_14(NPCs.ROCK_8109,"located near some ogres.",Location.create(2438, 3050, 0)), + CACTUS_4(NPCs.CACTUS_8107,"located in the southern desert.",Location.create(3252, 2963, 0)), + ROCK_15(NPCs.ROCK_8109,"located near some ogres.",Location.create(2340, 3064, 0)), + ROCK_16(NPCs.ROCK_8109,"located in the Wilderness.",Location.create(3108, 3837, 0)), + CACTUS_5(NPCs.CACTUS_8107,"located in the southern desert.",Location.create(3433, 3000, 0)), + CACTUS_6(NPCs.CACTUS_8107,"located in the southern desert.",Location.create(3274, 2813, 0)), + ROCK_17(NPCs.ROCK_8109,"located in the Wilderness.",Location.create(3019, 3866, 0)), + ROCK_18(NPCs.ROCK_8109,"located in the Wilderness.",Location.create(2991, 3824, 0)), + BUSH_21(NPCs.BUSH_8105,"located north of Ardougne.",Location.create(2398, 3361, 0)), + ROCK_19(NPCs.ROCK_8109,"located near the coast.",Location.create(2733, 3283, 0)), + ROCK_20(NPCs.ROCK_8109,"located in the Wilderness.",Location.create(3236, 3927, 0)); companion object { private val locationMap = values().map { it.location.toString() to it }.toMap() diff --git a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt index fb8a8d50e..53004f2b9 100644 --- a/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/DevelopmentCommandSet.kt @@ -10,6 +10,7 @@ import core.cache.def.impl.NPCDefinition import core.cache.def.impl.VarbitDefinition import core.cache.def.impl.Struct import core.game.node.entity.combat.ImpactHandler.HitsplatType +import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.game.node.entity.player.link.SpellBookManager import core.game.node.entity.player.link.diary.DiaryType @@ -30,7 +31,19 @@ import core.tools.Log import core.game.node.entity.player.info.Rights import core.game.node.entity.skill.Skills import core.game.world.map.Location +import core.game.world.map.RegionManager.getLocalEntitys import core.game.world.repository.Repository +import org.json.simple.JSONArray +import kotlin.collections.set +import content.global.activity.penguinhns.Penguin +import content.global.activity.penguinhns.PenguinHNSEvent +import content.global.activity.penguinhns.PenguinManager +import content.global.activity.penguinhns.PenguinManager.Companion.penguins +import content.global.activity.penguinhns.PenguinManager.Companion.spawner +import content.global.activity.penguinhns.PenguinManager.Companion.tagMapping +import content.global.activity.penguinhns.PenguinManager.Companion.updateStoreFile +import core.ServerStore.Companion.toJSONArray +import org.rs09.consts.NPCs @Initializable class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) { @@ -304,7 +317,7 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) { setAttribute (player, "routedraw", !getAttribute(player, "routedraw", false)) } - define ("fmstart", Privilege.ADMIN, description = "Marks your current tile as the force-movement start point.") {player, _ -> + define ("fmstart", Privilege.ADMIN, description = "Marks your current tile as the force-movement start point.") {player, _ -> setAttribute(player, "fmstart", Location.create(player.location)) } @@ -320,7 +333,7 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) { setAttribute(player, "fmspeedend", args[1].toIntOrNull() ?: 10) } - define("testfm", Privilege.ADMIN, description = "Runs the configured force-movement from the saved start to end point.") { player, _ -> + define("testfm", Privilege.ADMIN, description = "Runs the configured force-movement from the saved start to end point.") { player, _ -> val start = getAttribute(player, "fmstart", Location.create(player.location)) val end = getAttribute(player, "fmend", Location.create(player.location)) val speed = getAttribute(player, "fmspeed", 10) @@ -427,5 +440,54 @@ class DevelopmentCommandSet : CommandSet(Privilege.ADMIN) { target.skills.addExperience(skill, xp!!) } - } + + define("renewpenguins", Privilege.ADMIN, "", "Generates a fresh set of weekly penguins") { player, _ -> + val spawnedOrdinals = (PenguinHNSEvent.getStoreFile()["spawned-penguins"] as JSONArray).map { it.toString().toInt() } + val penguinNPCs = arrayListOf(NPCs.BARREL_8104, NPCs.BUSH_8105,NPCs.CACTUS_8107,NPCs.CRATE_8108,NPCs.ROCK_8109,NPCs.TOADSTOOL_8110) + + spawnedOrdinals.forEach { + val peng = Penguin.values()[it] + val nearNPCs = getLocalEntitys(peng.location,1) + nearNPCs.forEach { npc -> + if (npc.id in penguinNPCs) { + poofClear(npc as NPC) + } + } + } + penguins = spawner.spawnPenguins(10) + PenguinHNSEvent.getStoreFile()["spawned-penguins"] = penguins.toJSONArray() + tagMapping.clear() + for (p in penguins) { + tagMapping.put(p, JSONArray()) + val pengCoord = Penguin.values()[p].location + player.debug("Penguin spawned at:$pengCoord") + } + updateStoreFile() + player.debug("Penguin positions have been renewed") + } + + define("spawnpenguin",Privilege.ADMIN,"::spawnPenguin Ordinal","Adds a new Penguin spawn to this weeks list based on the ordinal provided 0-64"){player,args-> + if (args.size!=2) reject (player,"Usage: ::spawnpenguin Ordinal") + val ordinal = args[1].toIntOrNull() + if (ordinal == null) reject(player,"Ordinal must be an integer.") + if (ordinal!! > 64 || ordinal < 0) reject(player,"Ordinal must be in the range 0-64 inclusive.") + + val store = PenguinHNSEvent.getStoreFile() + val ordinals = (store["spawned-penguins"] as? JSONArray) + ?.map { it.toString().toInt() } + ?.toMutableList() + ?: mutableListOf() + val peng = Penguin.values()[ordinal] + NPC(peng.id,peng.location) + .also { PenguinManager.npcs.add(it); it.isNeverWalks = true; it.isWalks = false }.init() + tagMapping.clear() + for (p in ordinals) { + tagMapping.put(p, JSONArray()) + } + updateStoreFile() + val pengCoords = peng.location + player.debug("Penguin spawned at:$pengCoords") + } + + } } From acd0a9e22faff8375b9c0872d390a8573e5ff7c9 Mon Sep 17 00:00:00 2001 From: dam <27978131-real_damighty@users.noreply.gitlab.com> Date: Sat, 30 May 2026 17:44:13 +0300 Subject: [PATCH 23/23] Implemented ::ge mute command for players to mute the global news announcement for GE trades --- Server/src/main/core/game/bots/ScriptAPI.kt | 15 +++++++-- Server/src/main/core/game/ge/GrandExchange.kt | 4 ++- .../system/command/sets/MiscCommandSet.kt | 32 +++++++++++++++---- .../core/game/world/repository/Repository.kt | 22 ++++++++++--- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/Server/src/main/core/game/bots/ScriptAPI.kt b/Server/src/main/core/game/bots/ScriptAPI.kt index 5a3306e4c..c2ac69649 100644 --- a/Server/src/main/core/game/bots/ScriptAPI.kt +++ b/Server/src/main/core/game/bots/ScriptAPI.kt @@ -502,7 +502,10 @@ class ScriptAPI(private val bot: Player) { } val canSell = GrandExchange.addBotOffer(actualId, itemAmt) if (canSell && saleIsBigNews(actualId, itemAmt)) { - Repository.sendNews(SERVER_GE_NAME + " just offered " + itemAmt + " " + ItemDefinition.forId(actualId).name.toLowerCase() + " on the GE.") + Repository.sendGrandExchangeNews( + SERVER_GE_NAME + " just offered " + itemAmt + " " + + ItemDefinition.forId(actualId).name.toLowerCase() + " on the GE." + ) } bot.bank.remove(Item(id, itemAmt)) bot.bank.refresh() @@ -532,7 +535,10 @@ class ScriptAPI(private val bot: Player) { } val canSell = GrandExchange.addBotOffer(actualId, itemAmt) if (canSell && saleIsBigNews(actualId, itemAmt)) { - Repository.sendNews(SERVER_GE_NAME + " just offered " + itemAmt + " " + ItemDefinition.forId(actualId).name.toLowerCase() + " on the GE.") + Repository.sendGrandExchangeNews( + SERVER_GE_NAME + " just offered " + itemAmt + " " + + ItemDefinition.forId(actualId).name.toLowerCase() + " on the GE." + ) } bot.bank.remove(item) bot.bank.refresh() @@ -568,7 +574,10 @@ class ScriptAPI(private val bot: Player) { 1517 -> continue 1519 -> continue 1521 -> continue - else -> sendNews(SERVER_GE_NAME + " just offered " + itemAmt + " " + ItemDefinition.forId(actualId).name.lowercase() + " on the GE.") + else -> Repository.sendGrandExchangeNews( + SERVER_GE_NAME + " just offered " + itemAmt + " " + + ItemDefinition.forId(actualId).name.lowercase() + " on the GE." + ) } } bot.bank.remove(item) diff --git a/Server/src/main/core/game/ge/GrandExchange.kt b/Server/src/main/core/game/ge/GrandExchange.kt index 03c01380a..4472ed2f1 100644 --- a/Server/src/main/core/game/ge/GrandExchange.kt +++ b/Server/src/main/core/game/ge/GrandExchange.kt @@ -221,7 +221,9 @@ class GrandExchange : StartupListener, Commands { //GrandExchangeRecords.getInstance(player).update(offer) if (offer.sell && !player.isArtificial) { - sendNews(player.username + " just offered " + offer.amount + " " + getItemName(offer.itemID) + " on the GE.") + Repository.sendGrandExchangeNews( + player.username + " just offered " + offer.amount + " " + getItemName(offer.itemID) + " on the GE." + ) } if (ServerConstants.I_AM_A_CHEATER) { diff --git a/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt b/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt index 51023b308..281491ae2 100644 --- a/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt @@ -25,6 +25,7 @@ import core.game.system.command.CommandMapping import core.game.system.command.Privilege import core.game.system.communication.CommunicationInfo import core.game.world.map.RegionManager +import core.game.world.repository.GE_NEWS_MUTE_ATTRIBUTE import core.game.world.map.build.DynamicRegion import core.game.world.repository.Repository import core.game.node.entity.combat.equipment.WeaponInterface @@ -252,21 +253,38 @@ class MiscCommandSet : CommandSet(Privilege.ADMIN){ } /** - * Shows the player a list of currently active GE sell offers + * Shows the player a list of currently active GE sell offers or toggles muting sell offer News announcements. */ - define("ge", Privilege.STANDARD, "::ge MODE (Modes: buying, selling, search, bots, botsearch)", "Various commands for viewing GE offers.") { player, args -> - if(args.size < 2){ - reject(player, "Usage: ::ge mode", "Available modes: buying, selling, search, bots, botsearch") + define( + "ge", + Privilege.STANDARD, + "::ge MODE (Modes: buying, selling, search, bots, botsearch, mute)", + "Various commands for viewing GE offers." + ) { player, args -> + if (args.size < 2) { + reject(player, "Usage: ::ge mode", "Available modes: buying, selling, search, bots, botsearch, mute") } - val mode = args[1] - when(mode){ + when (mode) { "buying" -> showGeBuy(player) "selling" -> showGeSell(player) "search" -> showGeInputDialogue(player, args, ::showOffers) "bots" -> showGeBots(player) "botsearch" -> showGeInputDialogue(player, args, ::showGeBotsearch) - else -> reject(player, "Invalid mode used. Available modes are: buying, selling, search") + "mute" -> { + val currentlyMuted = getAttribute(player, GE_NEWS_MUTE_ATTRIBUTE, false) + if (currentlyMuted) { + removeAttribute(player, GE_NEWS_MUTE_ATTRIBUTE) + } else { + setAttribute(player, GE_NEWS_MUTE_ATTRIBUTE, true) + } + sendMessage(player, "GE sell offer news is now ${if (currentlyMuted) "visible" else "hidden"}.") + } + + else -> reject( + player, + "Invalid mode used. Available modes are: buying, selling, search, bots, botsearch, mute" + ) } } /** diff --git a/Server/src/main/core/game/world/repository/Repository.kt b/Server/src/main/core/game/world/repository/Repository.kt index 81d44d5f4..0b97f197c 100644 --- a/Server/src/main/core/game/world/repository/Repository.kt +++ b/Server/src/main/core/game/world/repository/Repository.kt @@ -1,16 +1,17 @@ package core.game.world.repository -import core.game.node.entity.npc.NPC import content.region.wilderness.handlers.revenants.RevenantNPC +import core.ServerConstants +import core.api.sendMessage +import core.game.node.entity.npc.NPC import core.game.node.entity.player.Player import core.game.world.map.Location import core.game.world.map.RegionManager -import core.ServerConstants -import core.api.sendMessage import core.game.world.update.UpdateSequence -import java.util.* import java.util.concurrent.CopyOnWriteArrayList +const val GE_NEWS_MUTE_ATTRIBUTE = "/save:ge:news-mute" + /** * The repository holding all node lists, etc in the game world. * @author Emperor @@ -64,11 +65,22 @@ object Repository { */ @JvmStatic fun sendNews(string: String, icon: Int = 12, color: String = "CC6600") { + sendNews(string, icon, color, false) + } + + @JvmStatic + fun sendGrandExchangeNews(string: String, icon: Int = 12, color: String = "CC6600") { + sendNews(string, icon, color, true) + } + + @JvmStatic + fun sendNews(string: String, icon: Int, color: String, isGeNews: Boolean) { if (!ServerConstants.ENABLE_GLOBAL_CHAT) return val players: Array = playerNames.values.toTypedArray() val size = players.size for (i in 0 until size) { - val player = players[i] as Player ?: continue + val player = players[i] as? Player ?: continue + if (isGeNews && player.getAttribute(GE_NEWS_MUTE_ATTRIBUTE, false)) continue sendMessage(player, "News: $string") } }