diff --git a/Server/src/main/content/global/handlers/iface/AutocastSelectPlugin.java b/Server/src/main/content/global/handlers/iface/AutocastSelectPlugin.java index a010c7bb5..9213cf0f6 100644 --- a/Server/src/main/content/global/handlers/iface/AutocastSelectPlugin.java +++ b/Server/src/main/content/global/handlers/iface/AutocastSelectPlugin.java @@ -30,6 +30,17 @@ public final class AutocastSelectPlugin extends ComponentPlugin { if (!player.getAttribute("autocast_select", false)) { return true; } + if (player.timers.getTimer("spellbook:swap") != null) { + player.getPacketDispatch().sendMessage("You cannot autocast while using Spellbook Swap."); + player.removeAttribute("autocast_select"); + player.removeAttribute("autocast_component"); + final WeaponInterface w = player.getExtension(WeaponInterface.class); + if (w != null) { + w.setAttackStyle(3); + player.getInterfaceManager().openTab(w); + } + return true; + } player.removeAttribute("autocast_select"); final WeaponInterface w = player.getExtension(WeaponInterface.class); if (w != null) { diff --git a/Server/src/main/content/global/skill/magic/MagicAltarListener.kt b/Server/src/main/content/global/skill/magic/MagicAltarListener.kt index eb4e61fc5..70ec07e25 100644 --- a/Server/src/main/content/global/skill/magic/MagicAltarListener.kt +++ b/Server/src/main/content/global/skill/magic/MagicAltarListener.kt @@ -1,15 +1,23 @@ package content.global.skill.magic -import core.api.* +import content.data.Quests +import core.api.animate +import core.api.hasLevelStat +import core.api.hasRequirement +import core.api.lock +import core.api.playAudio +import core.api.sendMessage +import core.game.event.SpellbookChangeEvent import core.game.interaction.IntType import core.game.interaction.InteractionListener import core.game.node.Node +import core.game.node.entity.combat.equipment.WeaponInterface import core.game.node.entity.player.Player +import core.game.node.entity.player.link.SpellBookManager import core.game.node.entity.player.link.SpellBookManager.SpellBook import core.game.node.entity.skill.Skills import org.rs09.consts.Scenery import org.rs09.consts.Sounds -import content.data.Quests class MagicAltarListener : InteractionListener { override fun defineListeners() { @@ -36,26 +44,36 @@ class MagicAltarListener : InteractionListener { return true } - - private fun swapSpellBook(player: Player, altar: Node) { - lock(player, 3) - playAudio(player, Sounds.PRAYER_RECHARGE_2674) - animate(player, 645) - - if (altar.id == ANCIENT_ALTAR) { - player.skills.decrementPrayerPoints(player.skills.prayerPoints) - } - - if (SpellBook.forInterface(player.spellBookManager.spellBook) == if (altar.id == ANCIENT_ALTAR) SpellBook.ANCIENT else SpellBook.LUNAR) { - sendMessage(player, if (altar.id == ANCIENT_ALTAR) "You feel a strange drain upon your memory..." else "Modern spells activated!") - player.spellBookManager.setSpellBook(SpellBook.MODERN) - player.spellBookManager.update(player) - } else { - sendMessage(player, if (altar.id == ANCIENT_ALTAR) "You feel a strange wisdom fill your mind..." else "Lunar spells activated!") - player.spellBookManager.setSpellBook(if (altar.id == ANCIENT_ALTAR) SpellBook.ANCIENT else SpellBook.LUNAR) - player.spellBookManager.update(player) - } - } + + private fun swapSpellBook(player : Player, altar : Node) { + lock(player, 3) + playAudio(player, Sounds.PRAYER_RECHARGE_2674) + animate(player, 645) + if (altar.id == ANCIENT_ALTAR) { + player.skills.decrementPrayerPoints(player.skills.prayerPoints) + } + val weaponInterface = player.getExtension(WeaponInterface::class.java) + if (weaponInterface != null && player.properties.autocastSpell != null) { + weaponInterface.selectAutoSpell(-1, true) + } + if (SpellBook.forInterface(player.spellBookManager.spellBook) == if (altar.id == ANCIENT_ALTAR) SpellBook.ANCIENT else SpellBook.LUNAR) { + player.dispatch(SpellbookChangeEvent( + SpellBook.forInterface(player.spellBookManager.spellBook), + SpellBook.MODERN, + SpellBookManager.SpellbookChangeSource.ALTAR)) + sendMessage(player, if (altar.id == ANCIENT_ALTAR) "You feel a strange drain upon your memory..." else "Modern spells activated!") + player.spellBookManager.setSpellBook(SpellBook.MODERN) + player.spellBookManager.update(player) + } else { + player.dispatch(SpellbookChangeEvent( + SpellBook.forInterface(player.spellBookManager.spellBook), + if (altar.id == ANCIENT_ALTAR) SpellBook.ANCIENT else SpellBook.LUNAR, + SpellBookManager.SpellbookChangeSource.ALTAR)) + sendMessage(player, if (altar.id == ANCIENT_ALTAR) "You feel a strange wisdom fill your mind..." else "Lunar spells activated!") + player.spellBookManager.setSpellBook(if (altar.id == ANCIENT_ALTAR) SpellBook.ANCIENT else SpellBook.LUNAR) + player.spellBookManager.update(player) + } + } companion object { private const val ANCIENT_ALTAR = Scenery.ALTAR_6552 diff --git a/Server/src/main/content/global/skill/magic/SpellUtils.kt b/Server/src/main/content/global/skill/magic/SpellUtils.kt index a80d646a5..beafbf64d 100644 --- a/Server/src/main/content/global/skill/magic/SpellUtils.kt +++ b/Server/src/main/content/global/skill/magic/SpellUtils.kt @@ -1,13 +1,25 @@ package content.global.skill.magic -import core.game.node.entity.npc.NPC -import core.game.node.entity.player.Player 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 object SpellUtils { + /** + * Validates that the spell packet's interface matches the player's actual server-side spellbook. + * This prevents exploits where the client has a stale spellbook interface (e.g., after Spellbook Swap reverts). + * @param player The player casting the spell + * @param packetInterfaceId The interface ID from the client's spell packet + * @return true if the interfaces match (valid), false if there's a client/server desync + */ + @JvmStatic + fun validateSpellbookInterface(player : Player, packetInterfaceId : Int) : Boolean { + val actualSpellbook = player.spellBookManager.spellBook + return packetInterfaceId == actualSpellbook + } fun usingStaff(p: Player, rune: Int): Boolean { val weapon = p.equipment[3] ?: return false val staff = MagicStaff.forId(rune) ?: return false diff --git a/Server/src/main/content/global/skill/magic/SpellbookSwapDialogue.java b/Server/src/main/content/global/skill/magic/SpellbookSwapDialogue.java index 8f9a62c8e..b794a87e0 100644 --- a/Server/src/main/content/global/skill/magic/SpellbookSwapDialogue.java +++ b/Server/src/main/content/global/skill/magic/SpellbookSwapDialogue.java @@ -1,10 +1,21 @@ package content.global.skill.magic; +import content.data.Quests; import core.game.component.Component; import core.game.dialogue.DialoguePlugin; +import core.game.event.SpellbookChangeEvent; +import core.game.interaction.Listener; import core.game.node.entity.player.Player; -import core.plugin.Initializable; import core.game.node.entity.player.link.SpellBookManager.SpellBook; +import core.game.node.entity.player.link.SpellBookManager.SpellbookChangeSource; +import core.game.node.entity.skill.Skills; +import core.game.node.item.Item; +import core.plugin.Initializable; + +import java.util.ArrayList; + +import static core.api.ContentAPIKt.hasRequirement; + /** * Handles the SpellbookSwapDialogue dialogue. @@ -73,8 +84,27 @@ public class SpellbookSwapDialogue extends DialoguePlugin { break; } final SpellBook book = type == 1 ? SpellBook.ANCIENT : SpellBook.MODERN; + if (book == SpellBook.ANCIENT && !hasRequirement(player, Quests.DESERT_TREASURE)) { + player.getPacketDispatch().sendMessage("You need to complete Desert Treasure to use Ancient Magicks."); + end(); + return true; + } + // Remove runes for Spellbook Swap + ArrayList runes = player.getAttribute("spell:runes", new ArrayList<>()); + if (!runes.isEmpty() && player.getInventory().remove(runes.toArray(new Item[0]))) { + player.removeAttribute("spell:runes"); + player.removeAttribute("tablet-spell"); + } + // Award XP + player.skills.addExperience(Skills.MAGIC, 130); + // Change book + player.dispatch(new SpellbookChangeEvent( + SpellBook.LUNAR, + book, + SpellbookChangeSource.SPELLBOOK_SWAP_CAST)); player.getSpellBookManager().setSpellBook(book); player.getInterfaceManager().openTab(new Component(book.getInterfaceId())); + player.getPacketDispatch().sendMessage("You have 2 minutes before your spellbook changes back to the Lunar Spellbook!"); end(); break; } diff --git a/Server/src/main/content/global/skill/magic/ancient/AncientTeleportPlugin.java b/Server/src/main/content/global/skill/magic/ancient/AncientTeleportPlugin.java index 8b8e2c693..a9c95ab5e 100644 --- a/Server/src/main/content/global/skill/magic/ancient/AncientTeleportPlugin.java +++ b/Server/src/main/content/global/skill/magic/ancient/AncientTeleportPlugin.java @@ -1,14 +1,15 @@ package content.global.skill.magic.ancient; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.link.diary.DiaryType; -import core.game.node.entity.combat.spell.MagicSpell; -import core.game.node.entity.combat.spell.Runes; +import core.game.event.SpellCastEvent; import core.game.node.Node; import core.game.node.entity.Entity; +import core.game.node.entity.combat.spell.MagicSpell; +import core.game.node.entity.combat.spell.Runes; import core.game.node.entity.combat.spell.SpellType; +import core.game.node.entity.player.Player; import core.game.node.entity.player.link.SpellBookManager.SpellBook; import core.game.node.entity.player.link.TeleportManager.TeleportType; +import core.game.node.entity.player.link.diary.DiaryType; import core.game.node.item.Item; import core.game.world.GameWorld; import core.game.world.map.Location; @@ -67,6 +68,7 @@ public final class AncientTeleportPlugin extends MagicSpell { return false; } homeTeleport((Player) entity, Location.create(3087, 3495, 0)); + entity.dispatch(new SpellCastEvent(SpellBook.ANCIENT, getSpellId(), target)); } else if (entity.getTeleporter().send(location.transform(0, RandomFunction.random(3), 0), TeleportType.ANCIENT)) { if (!super.meetsRequirements(entity, true, true)) { entity.getTeleporter().getCurrentTeleport().stop(); diff --git a/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt b/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt index 3654d666d..8d70f99cb 100644 --- a/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt +++ b/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt @@ -24,12 +24,17 @@ import core.game.system.config.NPCConfigParser import core.game.system.task.Pulse import core.game.system.timer.impl.PoisonImmunity import core.game.system.timer.impl.SkillRestore +import core.game.system.timer.impl.SpellbookSwap import core.game.world.map.Location import core.game.world.map.RegionManager import core.game.world.repository.Repository import core.game.world.update.flag.context.Animation import core.tools.RandomFunction -import org.rs09.consts.* +import org.rs09.consts.Animations +import org.rs09.consts.Components +import org.rs09.consts.Graphics +import org.rs09.consts.Items +import org.rs09.consts.Sounds import kotlin.math.floor class LunarListeners : SpellListener("lunar"), Commands { @@ -280,9 +285,14 @@ class LunarListeners : SpellListener("lunar"), Commands { */ // Level 96 - /** - * Spellbook Swap - */ + onCast(Lunar.SPELLBOOK_SWAP, NONE) { player, _ -> + requires(player, 96, arrayOf( + Item(Items.LAW_RUNE_563, 1), + Item(Items.COSMIC_RUNE_564, 2), + Item(Items.ASTRAL_RUNE_9075, 3) + )) + spellbookSwap(player) + } } // Spell handlers @@ -783,11 +793,20 @@ class LunarListeners : SpellListener("lunar"), Commands { /** * Heal Group */ - - // Level 96 - /** - * Spellbook Swap - */ + + // Level 96 + private fun spellbookSwap(player : Player) { + // Runes are removed on successful swap to another spellbook in SpellbookSwapDialogue.java + // removeRunes(player, true) + lock(player, 4) + visualizeSpell(player, 6299, 1062) + player.scripts.removeWeakScripts() + player.dialogueInterpreter.open(3264731) + registerTimer(player, SpellbookSwap()) + // XP is awarded in SpellbookSwapDialogue.java similarly to rune removal handling + // addXP(player, 130.0) + setDelay(player, false) + } // Other/Multi spell use-case private fun sendTeleport(player: Player, xp: Double, loc: Location){ diff --git a/Server/src/main/content/global/skill/magic/lunar/SpellbookSwapSpell.java b/Server/src/main/content/global/skill/magic/lunar/SpellbookSwapSpell.java deleted file mode 100644 index 4c152b0af..000000000 --- a/Server/src/main/content/global/skill/magic/lunar/SpellbookSwapSpell.java +++ /dev/null @@ -1,84 +0,0 @@ -package content.global.skill.magic.lunar; - -import core.game.component.Component; -import core.game.node.entity.combat.spell.MagicSpell; -import core.game.node.entity.combat.spell.Runes; -import core.game.node.Node; -import core.game.node.entity.Entity; -import core.game.node.entity.combat.spell.SpellType; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.link.SpellBookManager.SpellBook; -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 core.game.world.update.flag.context.Graphics; -import core.plugin.Initializable; -import core.plugin.Plugin; -import core.tools.RandomFunction; - -/** - * The spellbook swap spell. - * @author 'Vexia - */ -@Initializable -public class SpellbookSwapSpell extends MagicSpell { - - /** - * Represents the animation of this spell. - */ - private final Animation ANIMATION = new Animation(6299); - - /** - * Represents the graphics of this spell. - */ - private final Graphics GRAPHIC = new Graphics(1062); - - /** - * Constructs a new {@code SpellbookSwapSpell} {@code Object}. - */ - public SpellbookSwapSpell() { - super(SpellBook.LUNAR, 96, 130, null, null, null, new Item[] { new Item(Runes.LAW_RUNE.getId(), 1), new Item(Runes.COSMIC_RUNE.getId(), 2), new Item(Runes.ASTRAL_RUNE.getId(), 3) }); - } - - @Override - public Plugin newInstance(SpellType arg) throws Throwable { - SpellBook.LUNAR.register(12, this); - return this; - } - - @Override - public boolean cast(Entity entity, Node target) { - final Player player = (Player) entity; - if (!super.meetsRequirements(player, true, true)) { - return false; - } - player.lock(9); - player.animate(ANIMATION); - player.graphics(GRAPHIC); - player.getDialogueInterpreter().open(3264731); - final int id = RandomFunction.random(1, 500000); - player.setAttribute("spell:swap", id); - GameWorld.getPulser().submit(new Pulse(20, player) { - @Override - public boolean pulse() { - if (player.getAttribute("spell:swap", 0) == id) { - removeTemporarySpell(player); - } - return true; - } - - }); - return true; - } - - /** - * Method used to remove the temp spell swap. - * @param player the player. - */ - public static void removeTemporarySpell(final Player player) { - player.removeAttribute("spell:swap"); - player.getSpellBookManager().setSpellBook(SpellBook.LUNAR); - player.getInterfaceManager().openTab(new Component(SpellBook.LUNAR.getInterfaceId())); - } -} diff --git a/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt b/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt index eae541c70..7bb35b190 100644 --- a/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt +++ b/Server/src/main/content/global/skill/skillcapeperks/SkillcapePerks.kt @@ -1,23 +1,29 @@ package content.global.skill.skillcapeperks -import core.game.component.Component -import core.game.node.entity.player.Player -import core.game.node.entity.player.link.SpellBookManager -import core.game.node.entity.player.link.TeleportManager +import content.data.Quests +import content.global.skill.farming.PatchType +import content.global.skill.farming.Plantable import content.global.skill.runecrafting.Altar -import core.game.world.map.Location -import core.game.world.map.zone.impl.DarkZone -import core.plugin.Initializable -import core.game.world.GameWorld -import content.global.skill.farming.* import core.ServerStore import core.ServerStore.Companion.getBoolean import core.ServerStore.Companion.getInt -import core.api.* +import core.api.closeDialogue +import core.api.getAttribute +import core.api.hasRequirement +import core.api.sendDialogue +import core.api.sendMessage +import core.api.teleport import core.cache.def.impl.ItemDefinition -import core.tools.END_DIALOGUE -import org.rs09.consts.Items -import content.data.Quests +import core.game.component.Component +import core.game.event.SpellbookChangeEvent +import core.game.node.entity.combat.equipment.WeaponInterface +import core.game.node.entity.player.Player +import core.game.node.entity.player.link.SpellBookManager +import core.game.node.entity.player.link.TeleportManager +import core.game.world.GameWorld +import core.game.world.map.Location +import core.game.world.map.zone.impl.DarkZone +import core.plugin.Initializable enum class SkillcapePerks(val attribute: String, val effect: ((Player) -> Unit)? = null) { BAREFISTED_SMITHING("cape_perks:barefisted-smithing"), @@ -157,70 +163,74 @@ enum class SkillcapePerks(val attribute: String, val effect: ((Player) -> Unit)? } @Initializable - class MagicCapeDialogue(player: Player? = null): core.game.dialogue.DialoguePlugin(player){ - override fun newInstance(player: Player?): core.game.dialogue.DialoguePlugin { - return MagicCapeDialogue(player) - } + class MagicCapeDialogue(player : Player? = null) : core.game.dialogue.DialoguePlugin(player) { + override fun newInstance(player : Player?) : core.game.dialogue.DialoguePlugin { + return MagicCapeDialogue(player) + } - override fun open(vararg args: Any?): Boolean { - when(player.spellBookManager.spellBook){ - SpellBookManager.SpellBook.ANCIENT.interfaceId -> options("Modern","Lunar") - SpellBookManager.SpellBook.MODERN.interfaceId -> options("Ancient","Lunar") - SpellBookManager.SpellBook.LUNAR.interfaceId -> options ("Modern","Ancient") - } - return true - } + override fun open(vararg args : Any?) : Boolean { + when (player.spellBookManager.spellBook) { + SpellBookManager.SpellBook.ANCIENT.interfaceId -> options("Modern", "Lunar") + SpellBookManager.SpellBook.MODERN.interfaceId -> options("Ancient", "Lunar") + SpellBookManager.SpellBook.LUNAR.interfaceId -> options("Modern", "Ancient") + } + return true + } - override fun handle(interfaceId: Int, buttonId: Int): Boolean { - val spellbook = when(player.spellBookManager.spellBook){ - SpellBookManager.SpellBook.ANCIENT.interfaceId -> { - when(buttonId){ - 1 -> SpellBookManager.SpellBook.MODERN - 2 -> SpellBookManager.SpellBook.LUNAR - else -> null - } - } + override fun handle(interfaceId : Int, buttonId : Int) : Boolean { + val spellbook = when (player.spellBookManager.spellBook) { + SpellBookManager.SpellBook.ANCIENT.interfaceId -> { + when (buttonId) { + 1 -> SpellBookManager.SpellBook.MODERN + 2 -> SpellBookManager.SpellBook.LUNAR + else -> null + } + } - SpellBookManager.SpellBook.MODERN.interfaceId -> { - when(buttonId){ - 1 -> SpellBookManager.SpellBook.ANCIENT - 2 -> SpellBookManager.SpellBook.LUNAR - else -> null - } - } + SpellBookManager.SpellBook.MODERN.interfaceId -> { + when (buttonId) { + 1 -> SpellBookManager.SpellBook.ANCIENT + 2 -> SpellBookManager.SpellBook.LUNAR + else -> null + } + } - SpellBookManager.SpellBook.LUNAR.interfaceId -> { - when(buttonId){ - 1 -> SpellBookManager.SpellBook.MODERN - 2 -> SpellBookManager.SpellBook.ANCIENT - else -> null - } - } + SpellBookManager.SpellBook.LUNAR.interfaceId -> { + when (buttonId) { + 1 -> SpellBookManager.SpellBook.MODERN + 2 -> SpellBookManager.SpellBook.ANCIENT + else -> null + } + } - else -> null - } + else -> null + } - end() - if(spellbook != null){ - if (spellbook == SpellBookManager.SpellBook.ANCIENT) { - if (!hasRequirement(player, Quests.DESERT_TREASURE)) - return true - } - else if (spellbook == SpellBookManager.SpellBook.LUNAR) { - if (!hasRequirement(player, Quests.LUNAR_DIPLOMACY)) - return true - } - player.spellBookManager.setSpellBook(spellbook) - player.interfaceManager.openTab(Component(spellbook.interfaceId)) - player.incrementAttribute("/save:cape_perks:librarian-magus-charges",-1) - } - return true - } - - override fun getIds(): IntArray { - return intArrayOf(509871234) - } + end() + if (spellbook != null) { + if (spellbook == SpellBookManager.SpellBook.ANCIENT) { + if (!hasRequirement(player, Quests.DESERT_TREASURE)) return true + } else if (spellbook == SpellBookManager.SpellBook.LUNAR) { + if (!hasRequirement(player, Quests.LUNAR_DIPLOMACY)) return true + } + val weaponInterface = player.getExtension(WeaponInterface::class.java) + if (weaponInterface != null && player.properties.autocastSpell != null) { + weaponInterface.selectAutoSpell(-1, true) + } + player.dispatch(SpellbookChangeEvent( + SpellBookManager.SpellBook.forInterface(player.spellBookManager.spellBook), + spellbook, + SpellBookManager.SpellbookChangeSource.MAGIC_CAPE_PERK)) + player.spellBookManager.setSpellBook(spellbook) + player.interfaceManager.openTab(Component(spellbook.interfaceId)) + player.incrementAttribute("/save:cape_perks:librarian-magus-charges", -1) + } + return true + } + override fun getIds() : IntArray { + return intArrayOf(509871234) + } } @Initializable diff --git a/Server/src/main/core/api/Event.kt b/Server/src/main/core/api/Event.kt index 1c1bc1ea1..a572dc4bb 100644 --- a/Server/src/main/core/api/Event.kt +++ b/Server/src/main/core/api/Event.kt @@ -23,6 +23,7 @@ object Event { @JvmStatic val AttributeSet = AttributeSetEvent::class.java @JvmStatic val AttributeRemoved = AttributeRemoveEvent::class.java @JvmStatic val SpellCast = SpellCastEvent::class.java + @JvmStatic val SpellbookChanged = SpellbookChangeEvent::class.java @JvmStatic val ItemAlchemized = ItemAlchemizationEvent::class.java @JvmStatic val ItemEquipped = ItemEquipEvent::class.java @JvmStatic val ItemUnequipped = ItemUnequipEvent::class.java diff --git a/Server/src/main/core/game/event/Events.kt b/Server/src/main/core/game/event/Events.kt index 900b41629..b11d6241b 100644 --- a/Server/src/main/core/game/event/Events.kt +++ b/Server/src/main/core/game/event/Events.kt @@ -6,6 +6,7 @@ import core.game.node.Node import core.game.node.entity.Entity import core.game.node.entity.npc.NPC import core.game.node.entity.player.link.SpellBookManager.SpellBook +import core.game.node.entity.player.link.SpellBookManager.SpellbookChangeSource import core.game.node.entity.player.link.TeleportManager.TeleportType import core.game.node.entity.player.link.prayer.PrayerType import core.game.node.item.Item @@ -34,6 +35,7 @@ data class InterfaceCloseEvent(val component: Component) : Event data class AttributeSetEvent(val entity: Entity, val attribute: String, val value: Any) : Event data class AttributeRemoveEvent(val entity: Entity, val attribute: String) : Event data class SpellCastEvent(val spellBook: SpellBook, val spellId: Int, val target: Node? = null) : Event +data class SpellbookChangeEvent(val oldSpellBook: SpellBook, val newSpellBook: SpellBook, val source: SpellbookChangeSource) : Event data class ItemAlchemizationEvent(val itemId: Int, val isHigh: Boolean) : Event data class ItemEquipEvent(val itemId: Int, val slotId: Int) : Event data class ItemUnequipEvent(val itemId: Int, val slotId: Int) : Event 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 12059a76c..896a3ec62 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,8 +1,6 @@ package core.game.node.entity.combat.spell; -import core.game.component.Component; import core.game.event.SpellCastEvent; -import core.game.node.entity.skill.Skills; import core.game.node.Node; import core.game.node.entity.Entity; import core.game.node.entity.combat.CombatSwingHandler; @@ -10,6 +8,7 @@ import core.game.node.entity.combat.equipment.WeaponInterface; import core.game.node.entity.player.Player; import core.game.node.entity.player.link.SpellBookManager.SpellBook; import core.game.node.entity.player.link.audio.Audio; +import core.game.node.entity.skill.Skills; import core.game.node.item.Item; import core.game.world.GameWorld; import core.game.world.update.flag.context.Animation; @@ -121,11 +120,6 @@ public abstract class MagicSpell implements Plugin { p.faceTemporary((Entity) target, 1); } if (spell.cast(p, target)) { - if (book != SpellBook.LUNAR && p.getAttribute("spell:swap", 0) != 0) { - p.removeAttribute("spell:swap"); - p.getSpellBookManager().setSpellBook(SpellBook.LUNAR); - p.getInterfaceManager().openTab(new Component(SpellBook.LUNAR.getInterfaceId())); - } if (!combatSpell) { p.getSkills().addExperience(Skills.MAGIC, spell.getExperience(p), true); } diff --git a/Server/src/main/core/game/node/entity/player/link/SpellBookManager.java b/Server/src/main/core/game/node/entity/player/link/SpellBookManager.java index 7e0e456c4..72fd3005a 100644 --- a/Server/src/main/core/game/node/entity/player/link/SpellBookManager.java +++ b/Server/src/main/core/game/node/entity/player/link/SpellBookManager.java @@ -55,6 +55,16 @@ public final class SpellBookManager { return spellBook; } + /** + * All the possible ways a SpellBook can get changed. + */ + public enum SpellbookChangeSource { + ALTAR, + MAGIC_CAPE_PERK, + SPELLBOOK_SWAP_CAST, + SPELLBOOK_SWAP_RESTORE + } + /** * Represents a characters spell book. * @author 'Vexia @@ -134,5 +144,4 @@ public final class SpellBookManager { return spells.get(buttonId); } } - } \ No newline at end of file diff --git a/Server/src/main/core/game/system/timer/impl/SpellbookSwap.kt b/Server/src/main/core/game/system/timer/impl/SpellbookSwap.kt new file mode 100644 index 000000000..b689e3a0a --- /dev/null +++ b/Server/src/main/core/game/system/timer/impl/SpellbookSwap.kt @@ -0,0 +1,99 @@ +package core.game.system.timer.impl + +import core.api.Event.DialogueClosed +import core.api.Event.SpellCast +import core.api.Event.SpellbookChanged +import core.api.clearLogoutListener +import core.api.registerLogoutListener +import core.api.removeTimer +import core.game.event.* +import core.game.node.entity.Entity +import core.game.node.entity.combat.equipment.WeaponInterface +import core.game.node.entity.player.Player +import core.game.node.entity.player.link.SpellBookManager.SpellBook +import core.game.node.entity.player.link.SpellBookManager.SpellbookChangeSource +import core.game.system.timer.PersistTimer +import core.tools.minutesToTicks + +/** + * A timer dedicated to the handling of the Lunar spell Spellbook Swap. + */ +class SpellbookSwap : PersistTimer(runInterval = minutesToTicks(2), identifier = "spellbook:swap") { + + private val spellCastHook = object : EventHook { + override fun process(entity : Entity, event : SpellCastEvent) { + if (event.spellBook == SpellBook.LUNAR && event.spellId == 12) return // Ignore spellbook swap itself + revertSpellbook(entity) + removeTimer(entity) + } + } + + private val spellBookChangeHook = object : EventHook { + override fun process(entity : Entity, event : SpellbookChangeEvent) { + if (event.source != SpellbookChangeSource.SPELLBOOK_SWAP_CAST + && event.source != SpellbookChangeSource.SPELLBOOK_SWAP_RESTORE) { + removeTimer(entity) + } + } + } + + private val dialogueCloseHook = object : EventHook { + override fun process(entity : Entity, event : DialogueCloseEvent) { + if (event.dialogue?.getIds()?.contains(3264731) != true) return // Spellbook swap selection dialogue + if (entity !is Player) return + if (SpellBook.forInterface(entity.spellBookManager.spellBook) == SpellBook.LUNAR) { + entity.removeAttribute("spell:runes") + removeTimer(entity) + } + } + } + + override fun run(entity : Entity) : Boolean { + revertSpellbook(entity) + return false + } + + override fun onRegister(entity : Entity) { + entity.hook(SpellCast, spellCastHook) + entity.hook(SpellbookChanged, spellBookChangeHook) + entity.hook(DialogueClosed, dialogueCloseHook) + if (entity is Player) { + registerLogoutListener(entity, "spellbook:swap") { player -> + revertSpellbook(player) + } + } + } + + override fun onRemoval(entity : Entity) { + entity.unhook(spellCastHook) + entity.unhook(spellBookChangeHook) + entity.unhook(dialogueCloseHook) + if (entity is Player) { + clearLogoutListener(entity, "spellbook:swap") + } + } + + private fun revertSpellbook(entity : Entity) { + if (entity !is Player) return + if (SpellBook.forInterface(entity.spellBookManager.spellBook) == SpellBook.LUNAR) return + // Clear autocast + val weaponInterface = entity.getExtension(WeaponInterface::class.java) + if (weaponInterface != null && entity.properties.autocastSpell != null) { + weaponInterface.selectAutoSpell(-1, true) + } + // Close autocast selection interface if it's still open + if (entity.getAttribute("autocast_select", false)) { + entity.removeAttribute("autocast_select") + entity.removeAttribute("autocast_component") + if (weaponInterface != null) { + entity.interfaceManager.openTab(weaponInterface) + } + } + entity.dispatch(SpellbookChangeEvent( + SpellBook.forInterface(entity.spellBookManager.spellBook), + SpellBook.LUNAR, + SpellbookChangeSource.SPELLBOOK_SWAP_RESTORE)) + entity.spellBookManager.setSpellBook(SpellBook.LUNAR) + entity.spellBookManager.update(entity) + } +} diff --git a/Server/src/main/core/net/packet/PacketProcessor.kt b/Server/src/main/core/net/packet/PacketProcessor.kt index d76b82db8..6ec7d04ba 100644 --- a/Server/src/main/core/net/packet/PacketProcessor.kt +++ b/Server/src/main/core/net/packet/PacketProcessor.kt @@ -1,62 +1,61 @@ package core.net.packet -import core.game.event.ButtonClickEvent +import content.global.ame.events.maze.MazeInterface +import content.global.handlers.iface.ge.StockMarket +import content.global.skill.magic.SpellListener +import content.global.skill.magic.SpellListeners +import content.global.skill.magic.SpellUtils +import content.global.skill.summoning.familiar.FamiliarSpecial +import core.ServerConstants import core.api.getAttribute +import core.api.log import core.api.sendMessage import core.api.tryPop +import core.api.utils.Vector import core.cache.def.impl.ItemDefinition import core.cache.def.impl.NPCDefinition import core.cache.def.impl.SceneryDefinition import core.game.container.Container import core.game.container.impl.BankContainer -import core.game.node.Node -import core.game.node.entity.player.Player -import core.game.node.entity.player.info.Rights -import core.game.node.entity.player.info.login.LoginConfiguration -import core.game.node.entity.player.link.SpellBookManager -import core.game.node.entity.combat.spell.MagicSpell -import content.global.ame.events.maze.MazeInterface -import content.global.skill.summoning.familiar.FamiliarSpecial -import core.game.node.item.GroundItemManager -import core.game.node.item.Item -import core.game.node.scenery.Scenery -import core.game.system.communication.ClanRank -import core.game.system.communication.CommunicationInfo -import core.game.system.task.Pulse -import core.game.world.map.Location -import core.game.world.map.RegionManager -import core.game.world.update.flag.context.ChatMessage -import core.game.world.update.flag.* -import core.net.amsc.MSPacketRepository -import core.net.packet.context.PlayerContext -import core.net.packet.out.ClearMinimapFlag -import org.rs09.consts.Components -import proto.management.ClanMessage -import proto.management.JoinClanRequest -import proto.management.LeaveClanRequest -import core.ServerConstants +import core.game.event.ButtonClickEvent import core.game.ge.GrandExchange.Companion.getOfferStats import core.game.ge.GrandExchange.Companion.getRecommendedPrice import core.game.ge.GrandExchangeOffer import core.game.ge.PriceIndex -import content.global.handlers.iface.ge.StockMarket -import content.global.skill.magic.SpellListener -import content.global.skill.magic.SpellListeners -import content.global.skill.magic.SpellUtils -import core.api.log import core.game.interaction.* +import core.game.node.Node +import core.game.node.entity.combat.spell.MagicSpell +import core.game.node.entity.player.Player import core.game.node.entity.player.info.LogType import core.game.node.entity.player.info.PlayerMonitor -import core.tools.SystemLogger +import core.game.node.entity.player.info.Rights +import core.game.node.entity.player.info.login.LoginConfiguration +import core.game.node.entity.player.link.SpellBookManager +import core.game.node.item.GroundItemManager +import core.game.node.item.Item +import core.game.node.scenery.Scenery import core.game.system.command.CommandSystem +import core.game.system.communication.ClanRank +import core.game.system.communication.CommunicationInfo import core.game.system.communication.GlobalChat +import core.game.system.task.Pulse import core.game.world.GameWorld +import core.game.world.map.Location +import core.game.world.map.RegionManager import core.game.world.repository.Repository +import core.game.world.update.flag.EntityFlag +import core.game.world.update.flag.context.ChatMessage +import core.net.amsc.MSPacketRepository +import core.net.packet.context.PlayerContext import core.net.packet.`in`.Packet import core.net.packet.`in`.RunScript +import core.net.packet.out.ClearMinimapFlag import core.tools.Log import core.worker.ManagementEvents -import core.api.utils.Vector +import org.rs09.consts.Components +import proto.management.ClanMessage +import proto.management.JoinClanRequest +import proto.management.LeaveClanRequest import java.io.PrintWriter import java.io.StringWriter import java.lang.Math.min @@ -423,6 +422,11 @@ object PacketProcessor { return if (player.getAttribute("magic:delay", -1) > GameWorld.ticks) return + // Validate spell packet interface matches player's actual spellbook + // This is to prevent player being able to cast spells when client has stale interface (e.g. after Spellbook swap reverts) + if (iface in intArrayOf(192, 193, 430)) { + if (!SpellUtils.validateSpellbookInterface(player, iface)) return + } val book = SpellUtils.getBookFromInterface(iface) if (book != "none") SpellListeners.run(child, type, book, player, target)