From 845d8b383b4a39ccd4e58eb6b3aab615fe9102cf Mon Sep 17 00:00:00 2001 From: Ceikry Date: Mon, 17 Jul 2023 08:12:57 +0000 Subject: [PATCH] Exception fixes Fixed a bug that could occasionally cause the sandwich lady interface to become unresponsive Fixed an issue that could cause shade random events to not spawn for max-level players Fixed an issue that could cause zombie random events to not spawn for max-level players Fixed a widespread book issue that resulted from not resetting the current page index when closing a book, and then trying to open another book with less pages. Converted bogrog pouch swap to the generic item select interface, and removed relevant code from the grand exchange code Fixed an issue that could occasionally prevent exchange of item sets Fixed an issue with Diango's reclaim interface that could result in items other than the ones you clicked being claimed Made incubation moderately more reliable, and incubation timers now pass even while offline Fixed an issue that prevented the leather crafting dialogue from accepting tokenized amounts (e.g. 100k) Fixed an issue that prevented the tool leprechaun interface from allowing tokenized amounts Fixed an issue that prevented players from using tokenized amounts in the ogleroot purchase dialogue for vinesweeper Fixed an issue that prevented Zaff's batttlestaff dialogue from accepting tokenized amounts Fixed an issue that prevented shop buy-x options from accepting tokenized amounts Various shop-related exception fixes Added some ingame player messages for when a Hunter "catch" option isn't being handled properly Fixed some exceptions being thrown by lunar spells Pyramid Plunder is now better at detecting when a player is in the minigame erroneously and kicking them out Fixed an issue that would lead to incorrect Kalphite Queen behavior when killing her with multiple players in the room Fixed an issue that could cause the Bedabin Nomad's dialogue to hang during Tourist Trap Fixed several more issues with Fight Arena cutscenes Fixed an issue that could prevent the dwarf dialogue from opening while trying to enter the mountain shortcut before finishing Fishing Contest Fixed an issue that could cause dialogue option hangs in certain unusual circumstances Fixed an issue that could result in non-movement for interactions where the override location was the same as the target node's location and there was no destination flag set Fixed an issue where setting invalid node data could cause issues with NPC rendering Server can now detect when a session is sending up multiple invalid packets and automatically disconnect the player Fixed an issue that could lead to unintended saving of varps/varbits Fixed an issue that could cause some interactions in shops to silently fail. The user will now receive a relevant message when these situations occur Users may now find they could occasionally receive a very snarky response when incorrectly using commands Users now receive messages when they try to make use of unimplemented friends/clan/etc features. Errors in string parsing will no longer result in "dead" inputs when attempting to add friends/update clan ranks/etc Fixed an issue that could lead to loss of some alerts in the Discord integration. Discord integration will now detect problems and queue the failed messages to be resent later. Added more safety checks to item-on-grounditem packet handling to prevent throwing exceptions Exceptions in string parsing for command packets will now inform the user that something went wrong, and ask them to try again. Improved drop table parsing and fixed incorrect drop tables --- Server/data/configs/drop_tables.json | 14 +- Server/data/configs/ground_spawns.json | 4 - .../sandwichlady/SandwichLadyInterface.kt | 10 +- .../global/ame/events/shade/ShadeRENPC.kt | 11 +- .../global/ame/events/zombie/ZombieRENPC.kt | 11 +- .../global/handlers/iface/BookInterface.kt | 2 +- .../handlers/iface/GenericItemSelect.kt | 9 +- .../iface/ge/GrandExchangeInterface.java | 21 +- .../global/handlers/iface/ge/StockMarket.kt | 6 +- .../item/toys/DiangoReclaimInterface.java | 32 +- .../item/withobject/IncubatorPlugin.java | 6 +- .../skill/construction/BuildOptionPlugin.java | 6 +- .../skill/crafting/LeatherCraftDialogue.java | 3 +- .../skill/farming/ToolLeprechaunInterface.kt | 2 +- .../global/skill/hunter/HunterPlugin.java | 8 +- .../skill/magic/lunar/LunarListeners.kt | 2 + .../skill/magic/lunar/StatRestoreSpell.java | 11 +- .../skill/summoning/pet/IncubatorState.kt | 26 +- .../minigame/mta/TelekineticGrabSpell.java | 2 + .../minigame/pyramidplunder/PlunderUtils.kt | 6 + .../pyramidplunder/PyramidPlunderMinigame.kt | 22 +- .../vinesweeper/VinesweeperDialogues.kt | 6 +- .../desert/handlers/KalphiteQueenNPC.java | 2 +- .../thetouristrap/BedabinNomadDialogue.java | 3 +- .../quest/arena/cutscenes/EscapeCutscene.kt | 8 +- .../gutanoth/handlers/BogrogPlugin.java | 9 +- .../gutanoth/handlers/BogrogPouchSwapper.kt | 33 +- .../fishingcontest/StairInteraction.java | 8 +- .../misthalin/varrock/handlers/ZaffPlugin.kt | 2 +- .../handlers/CorporealBeastNPC.java | 2 + Server/src/main/core/api/ContentAPI.kt | 24 +- Server/src/main/core/api/utils/Exceptions.kt | 3 + .../main/core/api/utils/WeightBasedTable.kt | 20 +- .../main/core/game/dialogue/DialogueFile.kt | 1 + .../game/dialogue/DialogueInterpreter.java | 5 +- .../core/game/dialogue/DialoguePlugin.java | 1 + .../game/dialogue/SimpleEntityMessage.java | 46 --- .../core/game/global/report/AbuseReport.java | 2 +- .../core/game/interaction/MovementPulse.java | 3 +- Server/src/main/core/game/node/Node.java | 2 + .../node/entity/npc/drop/NPCDropTables.java | 2 +- .../core/game/node/entity/player/Player.java | 10 + .../entity/player/info/login/PlayerSaver.kt | 2 + .../entity/player/link/InterfaceManager.java | 2 +- .../game/node/entity/state/StateRepository.kt | 4 +- .../game/node/scenery/SceneryBuilder.java | 2 - Server/src/main/core/game/shops/Shop.kt | 13 + Server/src/main/core/game/shops/Shops.kt | 10 +- .../system/command/sets/MiscCommandSet.kt | 4 + .../system/command/sets/MusicCommandSet.kt | 4 +- .../communication/CommunicationInfo.java | 16 + .../game/system/config/DropTableParser.kt | 35 +- .../main/core/integrations/discord/Discord.kt | 330 ++++++++++-------- .../core/net/amsc/MSPacketRepository.java | 7 +- .../main/core/net/event/GameReadEvent.java | 7 +- .../src/main/core/net/event/MSReadEvent.java | 3 +- .../main/core/net/packet/PacketProcessor.kt | 8 +- .../main/core/net/packet/in/Decoders530.kt | 12 +- Server/src/main/core/net/packet/in/Login.kt | 3 + .../src/main/core/net/packet/in/RunScript.kt | 12 +- Server/src/main/core/tools/StringUtils.java | 4 + 61 files changed, 547 insertions(+), 337 deletions(-) create mode 100644 Server/src/main/core/api/utils/Exceptions.kt delete mode 100644 Server/src/main/core/game/dialogue/SimpleEntityMessage.java diff --git a/Server/data/configs/drop_tables.json b/Server/data/configs/drop_tables.json index b1929f67c..d79fda2df 100644 --- a/Server/data/configs/drop_tables.json +++ b/Server/data/configs/drop_tables.json @@ -2438,7 +2438,7 @@ "minAmount": "75", "weight": "1.0", "id": "556", - "maxAmount": "5" + "maxAmount": "75" }, { "minAmount": "10", @@ -19681,10 +19681,10 @@ "maxAmount": "1" }, { - "minAmount": "110", + "minAmount": "30", "weight": "5.0", "id": "313", - "maxAmount": "30" + "maxAmount": "110" }, { "minAmount": "1", @@ -19846,10 +19846,10 @@ "maxAmount": "1" }, { - "minAmount": "110", + "minAmount": "30", "weight": "5.0", "id": "313", - "maxAmount": "30" + "maxAmount": "110" }, { "minAmount": "1", @@ -48483,10 +48483,10 @@ "maxAmount": "2" }, { - "minAmount": "3", + "minAmount": "1", "weight": "500.0", "id": "7054", - "maxAmount": "1" + "maxAmount": "3" }, { "minAmount": "1", diff --git a/Server/data/configs/ground_spawns.json b/Server/data/configs/ground_spawns.json index e8765d2d9..989bbc066 100644 --- a/Server/data/configs/ground_spawns.json +++ b/Server/data/configs/ground_spawns.json @@ -491,10 +491,6 @@ "item_id": "2347", "loc_data": "{1,2934,3286,0,7209050}-{1,2579,3463,0,5242920}-{1,2975,3368,1,5898270}-{1,1953,4974,0,5898270}" }, - { - "item_id": "2351", - "loc_data": "{1,3062,3657,0,11141270}" - }, { "item_id": "2357", "loc_data": "{1,3192,9822,0,11141270}" diff --git a/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt b/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt index 9b3c607df..306dc6a84 100644 --- a/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt +++ b/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt @@ -3,6 +3,7 @@ package content.global.ame.events.sandwichlady import core.game.node.item.Item import org.rs09.consts.Items import core.game.interaction.InterfaceListener +import content.global.ame.RandomEventManager class SandwichLadyInterface : InterfaceListener { @@ -17,6 +18,11 @@ class SandwichLadyInterface : InterfaceListener { override fun defineInterfaceListeners() { on(SANDWICH_INTERFACE){player, _, _, buttonID, _, _ -> + val event = RandomEventManager.getInstance(player)!!.event + if (event == null) { + player.interfaceManager.close() + return@on true + } val item = when(buttonID) { 7 -> {Item(baguette)} @@ -31,8 +37,8 @@ class SandwichLadyInterface : InterfaceListener { player.setAttribute("sandwich-lady:choice",item.id) player.interfaceManager.close() - player.dialogueInterpreter.open(SandwichLadyDialogue(true), content.global.ame.RandomEventManager.getInstance(player)!!.event) + player.dialogueInterpreter.open(SandwichLadyDialogue(true), event) return@on true } } -} \ No newline at end of file +} diff --git a/Server/src/main/content/global/ame/events/shade/ShadeRENPC.kt b/Server/src/main/content/global/ame/events/shade/ShadeRENPC.kt index 8fecda80c..4d991afdf 100644 --- a/Server/src/main/content/global/ame/events/shade/ShadeRENPC.kt +++ b/Server/src/main/content/global/ame/events/shade/ShadeRENPC.kt @@ -4,16 +4,15 @@ import core.game.node.entity.Entity import core.game.node.entity.npc.NPC import content.global.ame.RandomEventNPC import core.api.utils.WeightBasedTable - - -val ids = 425..430 +import kotlin.math.* class ShadeRENPC(override var loot: WeightBasedTable? = null) : RandomEventNPC(425){ + val ids = (425..430).toList() override fun talkTo(npc: NPC) {} override fun init() { super.init() - val index = (player.properties.combatLevel / 20) - 1 - val id = ids.toList()[index] + val index = max(0, min(ids.size, (player.properties.combatLevel / 20) - 1)) + val id = ids[index] this.transform(id) this.attack(player) sendChat("Leave this place!") @@ -29,4 +28,4 @@ class ShadeRENPC(override var loot: WeightBasedTable? = null) : RandomEventNPC(4 super.tick() if(!player.viewport.currentPlane.npcs.contains(this)) this.clear() } -} \ No newline at end of file +} diff --git a/Server/src/main/content/global/ame/events/zombie/ZombieRENPC.kt b/Server/src/main/content/global/ame/events/zombie/ZombieRENPC.kt index 1fed431b9..230d85ee8 100644 --- a/Server/src/main/content/global/ame/events/zombie/ZombieRENPC.kt +++ b/Server/src/main/content/global/ame/events/zombie/ZombieRENPC.kt @@ -4,16 +4,15 @@ import core.game.node.entity.Entity import core.game.node.entity.npc.NPC import content.global.ame.RandomEventNPC import core.api.utils.WeightBasedTable - - -val ids = 419..424 +import kotlin.math.* class ZombieRENPC(override var loot: WeightBasedTable? = null) : RandomEventNPC(419){ + val ids = (419..424).toList() override fun talkTo(npc: NPC) {} override fun init() { super.init() - val index = (player.properties.combatLevel / 20) - 1 - val id = ids.toList()[index] + val index = max(0, min(ids.size, (player.properties.combatLevel / 20) - 1)) + val id = ids[index] this.transform(id) this.attack(player) sendChat("Brainsssss!") @@ -29,4 +28,4 @@ class ZombieRENPC(override var loot: WeightBasedTable? = null) : RandomEventNPC( super.tick() if(!player.viewport.currentPlane.npcs.contains(this)) this.clear() } -} \ No newline at end of file +} diff --git a/Server/src/main/content/global/handlers/iface/BookInterface.kt b/Server/src/main/content/global/handlers/iface/BookInterface.kt index 270a90c9a..ccb9d228d 100644 --- a/Server/src/main/content/global/handlers/iface/BookInterface.kt +++ b/Server/src/main/content/global/handlers/iface/BookInterface.kt @@ -190,4 +190,4 @@ class Page(vararg lines: BookLine) { class BookLine( val message: String, val child: Int -) \ No newline at end of file +) diff --git a/Server/src/main/content/global/handlers/iface/GenericItemSelect.kt b/Server/src/main/content/global/handlers/iface/GenericItemSelect.kt index 48b4b3318..beb2a297e 100644 --- a/Server/src/main/content/global/handlers/iface/GenericItemSelect.kt +++ b/Server/src/main/content/global/handlers/iface/GenericItemSelect.kt @@ -30,6 +30,7 @@ class GenericItemSelect : InterfaceListener { onClose(GENERIC_ITEM_SELECT_IFACE) {player, _ -> removeAttribute(player, "itemselect-callback") + removeAttribute(player, "itemselect-keepalive") return@onClose true } } @@ -56,7 +57,11 @@ class GenericItemSelect : InterfaceListener { } callback.invoke(slot, optionIndex) - removeAttribute (player, "itemselect-callback") - player.interfaceManager.closeSingleTab() + + if (!getAttribute(player, "itemselect-keepalive", false)) { + removeAttribute (player, "itemselect-callback") + removeAttribute (player, "itemselect-keepalive") + player.interfaceManager.closeSingleTab() + } } } diff --git a/Server/src/main/content/global/handlers/iface/ge/GrandExchangeInterface.java b/Server/src/main/content/global/handlers/iface/ge/GrandExchangeInterface.java index a6fa904c9..dd01103aa 100644 --- a/Server/src/main/content/global/handlers/iface/ge/GrandExchangeInterface.java +++ b/Server/src/main/content/global/handlers/iface/ge/GrandExchangeInterface.java @@ -18,7 +18,6 @@ import core.plugin.Initializable; import core.plugin.Plugin; import core.game.ge.GrandExchangeOffer; import core.game.ge.GrandExchangeRecords; -import content.region.kandarin.feldip.gutanoth.handlers.BogrogPouchSwapper; import core.game.world.GameWorld; /** @@ -132,12 +131,20 @@ public class GrandExchangeInterface extends ComponentPlugin { if (slot < 0 || slot >= (inventory ? 28 : GEItemSet.values().length)) { return; } - GEItemSet set = GEItemSet.values()[slot]; - Item item = inventory ? player.getInventory().get(slot) : new Item(set.getItemId()); - if (item == null) { - return; - } - if (opcode != 127 && inventory && ((set = GEItemSet.forId(item.getId())) == null) && !BogrogPouchSwapper.handle(player,opcode,slot,itemId)) { + + Item item; + GEItemSet set; + if (inventory) { + item = player.getInventory().get(slot); + if (item == null) return; + set = GEItemSet.forId(item.getId()); + if (set == null) return; + } else { + set = GEItemSet.values()[slot]; + item = new Item(set.getItemId()); + } + + if (opcode != 127 && inventory && set == null) { player.getPacketDispatch().sendMessage("This isn't a set item."); return; } diff --git a/Server/src/main/content/global/handlers/iface/ge/StockMarket.kt b/Server/src/main/content/global/handlers/iface/ge/StockMarket.kt index 7879f75ba..52f1de8dc 100644 --- a/Server/src/main/content/global/handlers/iface/ge/StockMarket.kt +++ b/Server/src/main/content/global/handlers/iface/ge/StockMarket.kt @@ -159,15 +159,11 @@ class StockMarket : InterfaceListener { 183 -> updateOfferValue(player, tempOffer, (GrandExchange.getRecommendedPrice(tempOffer.itemID) * 1.05).toInt()) 171 -> updateOfferValue(player, tempOffer, tempOffer.offeredValue - 1) 173 -> updateOfferValue(player, tempOffer, tempOffer.offeredValue + 1) - 185 -> sendInputDialogue(player, false, "Enter the amount:"){value -> + 185 -> sendInputDialogue(player, InputType.AMOUNT, "Enter the amount:"){value -> if (player.interfaceManager.chatbox.id == 389) { player.interfaceManager.openChatbox(Components.OBJDIALOG_389) } var s = value.toString() - s = s.replace("k", "000") - s = s.replace("K", "000") - s = s.replace("m", "000000") - s = s.replace("M", "000000") updateOfferValue(player, tempOffer, s.toInt()) setAttribute(player, "ge-temp", tempOffer) } diff --git a/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java b/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java index 71d5294de..82afc70ab 100644 --- a/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java +++ b/Server/src/main/content/global/handlers/item/toys/DiangoReclaimInterface.java @@ -43,14 +43,10 @@ public class DiangoReclaimInterface extends ComponentPlugin { if(curOpen != null){ curOpen.close(player); } + Item[] reclaimables = getEligibleItems(player); + player.setAttribute("diango-reclaimables", reclaimables); //filter out items the player already has in their bank, inventory, or equipped - Item[] reclaimables = ITEMS.stream().filter(Objects::nonNull) - .filter(item -> !player.getEquipment().containsItem(item) && !player.getInventory().containsItem(item) && !player.getBank().containsItem(item) - && (item.getId() != 14654 - || (!(inInventory(player, 14655, 1) || inEquipment(player, 14656, 1)) && player.getAttribute("sotr:purchased",false)) - )) - .toArray(Item[]::new); //only send items if there are some to send if(reclaimables.length > 0) { @@ -68,18 +64,36 @@ public class DiangoReclaimInterface extends ComponentPlugin { @Override public boolean handle(Player player, Component component, int opcode, int button, int slot, int itemId) { - Item[] reclaimables = ITEMS.stream().filter(Objects::nonNull).filter(item -> !player.getEquipment().containsItem(item) && !player.getInventory().containsItem(item) && !player.getBank().containsItem(item)).toArray(Item[]::new); + Item[] reclaimables = player.getAttribute("diango-reclaimables", null); + if (reclaimables == null) + reclaimables = getEligibleItems(player); + + Item reclaimItem = reclaimables[slot]; + if (reclaimItem == null) { + player.sendMessage("Something went wrong there. Please try again."); + return true; + } + switch(opcode){ case 155: //interface item option 1 == take //add the clicked item to the player's inventory and refresh the interface - player.getInventory().add(reclaimables[slot]); + player.getInventory().add(reclaimItem); refresh(player); break; case 196: //interface item option 2 == examine //send the examine text for the item to the player - player.getPacketDispatch().sendMessage(reclaimables[slot].getDefinition().getExamine()); + player.getPacketDispatch().sendMessage(reclaimItem.getDefinition().getExamine()); break; } return false; } + + public static Item[] getEligibleItems (Player player) { + return ITEMS.stream().filter(Objects::nonNull) + .filter(item -> !player.getEquipment().containsItem(item) && !player.getInventory().containsItem(item) && !player.getBank().containsItem(item) + && (item.getId() != 14654 + || (!(inInventory(player, 14655, 1) || inEquipment(player, 14656, 1)) && player.getAttribute("sotr:purchased",false)) + )) + .toArray(Item[]::new); + } } diff --git a/Server/src/main/content/global/handlers/item/withobject/IncubatorPlugin.java b/Server/src/main/content/global/handlers/item/withobject/IncubatorPlugin.java index b0612293c..52c3b20da 100644 --- a/Server/src/main/content/global/handlers/item/withobject/IncubatorPlugin.java +++ b/Server/src/main/content/global/handlers/item/withobject/IncubatorPlugin.java @@ -34,7 +34,9 @@ public class IncubatorPlugin extends OptionHandler { @Override public boolean handle(Player player, Node node, String option) { - int inc = player.getAttribute("inc", -1); + int inc = player.getAttribute(IncubatorState.ATTR_INCUBATOR_PRODUCT, -1); + if (inc == -1) + inc = player.getAttribute("inc", -1); //deprecated, only here to avoid data loss switch (option) { case "take-egg": if (inc == -1) { @@ -54,6 +56,8 @@ public class IncubatorPlugin extends OptionHandler { GroundItemManager.create(egg.getProduct(),player); } player.removeAttribute("inc"); + player.removeAttribute(IncubatorState.ATTR_INCUBATOR_PRODUCT); + player.clearState("incubator"); } return true; case "inspect": diff --git a/Server/src/main/content/global/skill/construction/BuildOptionPlugin.java b/Server/src/main/content/global/skill/construction/BuildOptionPlugin.java index 3fb866ea6..69b0b1483 100644 --- a/Server/src/main/content/global/skill/construction/BuildOptionPlugin.java +++ b/Server/src/main/content/global/skill/construction/BuildOptionPlugin.java @@ -66,8 +66,8 @@ public final class BuildOptionPlugin extends OptionHandler { } Hotspot hotspot = player.getHouseManager().getHotspot(object); if (hotspot == null || !isBuildable(player, object, hotspot)) { - - log(this.getClass(), Log.ERR, "Construction (building): " + hotspot + " : " + object + " chunkX = " + object.getLocation().getChunkX() + ", chunkY = " + object.getLocation().getChunkY()); + //log demoted from ERR to WARN June 16, 2023 -> It's the effect of an already-known issue. Reduced visibility to clean up server logs while still making the issue apparent in dev environs + log(this.getClass(), Log.WARN, "Construction (building): " + hotspot + " : " + object + " chunkX = " + object.getLocation().getChunkX() + ", chunkY = " + object.getLocation().getChunkY()); return true; } @@ -181,4 +181,4 @@ public final class BuildOptionPlugin extends OptionHandler { } -} \ No newline at end of file +} diff --git a/Server/src/main/content/global/skill/crafting/LeatherCraftDialogue.java b/Server/src/main/content/global/skill/crafting/LeatherCraftDialogue.java index 7096ab935..0692339a6 100644 --- a/Server/src/main/content/global/skill/crafting/LeatherCraftDialogue.java +++ b/Server/src/main/content/global/skill/crafting/LeatherCraftDialogue.java @@ -1,6 +1,7 @@ package content.global.skill.crafting; import static core.api.ContentAPIKt.*; +import core.api.InputType; import core.cache.def.impl.ItemDefinition; import core.game.component.Component; import core.game.dialogue.DialoguePlugin; @@ -204,7 +205,7 @@ public final class LeatherCraftDialogue extends DialoguePlugin { if (hidee == null) { return false; } - sendInputDialogue(player, true, "Enter the amount:", (value) -> { + sendInputDialogue(player, InputType.AMOUNT, "Enter the amount:", (value) -> { player.getPulseManager().run(new DragonCraftPulse(player, null, hidee, (int) value)); return Unit.INSTANCE; }); diff --git a/Server/src/main/content/global/skill/farming/ToolLeprechaunInterface.kt b/Server/src/main/content/global/skill/farming/ToolLeprechaunInterface.kt index 175c2af71..9ef9aa7a7 100644 --- a/Server/src/main/content/global/skill/farming/ToolLeprechaunInterface.kt +++ b/Server/src/main/content/global/skill/farming/ToolLeprechaunInterface.kt @@ -181,7 +181,7 @@ class ToolLeprechaunInterface : InterfaceListener { player.dialogueInterpreter.sendDialogue("You don't have any of those stored.") } else { if(amount == -2){ - sendInputDialogue(player, true, "Enter the amount:"){value -> + sendInputDialogue(player, InputType.AMOUNT, "Enter the amount:"){value -> var amt = value as Int if(amt > hasAmount){ amt = hasAmount diff --git a/Server/src/main/content/global/skill/hunter/HunterPlugin.java b/Server/src/main/content/global/skill/hunter/HunterPlugin.java index 83cb8993b..b434c2287 100644 --- a/Server/src/main/content/global/skill/hunter/HunterPlugin.java +++ b/Server/src/main/content/global/skill/hunter/HunterPlugin.java @@ -303,7 +303,13 @@ public final class HunterPlugin extends OptionHandler { @Override public boolean handle(Player player, Node node, String option) { - BNetTypes.forNpc((NPC) node).handle(player, (NPC) node); + BNetTypes type = BNetTypes.forNpc((NPC) node); + if (type == null) { + player.sendMessage("There seems to be something wrong with this catch option."); + player.sendMessage("Please submit a detailed bug report on gitlab."); + return true; + } + type.handle(player, (NPC) node); return true; } 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 dbbc52752..d6a67dd90 100644 --- a/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt +++ b/Server/src/main/content/global/skill/magic/lunar/LunarListeners.kt @@ -387,6 +387,8 @@ class LunarListeners : SpellListener("lunar"), Commands { var counter = 0 override fun pulse(): Boolean { removeAttribute(player, "spell:runes") + if (playerJewellery.isEmpty()) + return true requires(player, 80, arrayOf(Item(Items.ASTRAL_RUNE_9075, 2), Item(Items.EARTH_RUNE_557, 10), Item(Items.WATER_RUNE_555, 5))) if(counter == 0) delay = animationDuration(STRING_JEWELLERY_ANIM) + 1 val item = playerJewellery[0] diff --git a/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java b/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java index 6fad08ad4..89d87f362 100644 --- a/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java +++ b/Server/src/main/content/global/skill/magic/lunar/StatRestoreSpell.java @@ -41,10 +41,17 @@ public class StatRestoreSpell extends MagicSpell { public boolean cast(Entity entity, Node target) { final Player player = ((Player) entity); Item item = ((Item) target); - final Potion potion = (Potion) Consumables.getConsumableById(item.getId()).getConsumable(); player.getInterfaceManager().setViewedTab(6); + + if (Consumables.getConsumableById(item.getId()) == null) { + player.getPacketDispatch().sendMessage("You can only cast this spell on a potion."); + return false; + } + + final Potion potion = (Potion) Consumables.getConsumableById(item.getId()).getConsumable(); + if (potion == null) { - player.getPacketDispatch().sendMessage("You can only cast this spell on a potion."); + player.getPacketDispatch().sendMessage("You can only cast this spell on a potion."); return false; } if (!item.getDefinition().isTradeable() || !isRestore(potion)) { diff --git a/Server/src/main/content/global/skill/summoning/pet/IncubatorState.kt b/Server/src/main/content/global/skill/summoning/pet/IncubatorState.kt index 72ff9d086..dfe5adf8e 100644 --- a/Server/src/main/content/global/skill/summoning/pet/IncubatorState.kt +++ b/Server/src/main/content/global/skill/summoning/pet/IncubatorState.kt @@ -6,22 +6,27 @@ import core.game.node.entity.state.State import core.game.system.task.Pulse import org.json.simple.JSONObject import core.api.* +import core.tools.* @PlayerState("incubator") class IncubatorState(player: Player? = null) : State(player) { var egg: IncubatorEgg? = null - var ticksLeft = 0 + var completionTimeMs = 0L + + fun setTicksLeft (ticks: Int) { + completionTimeMs = System.currentTimeMillis() + (ticksToSeconds(ticks) * 1000) + } override fun newInstance(player: Player?): State { return IncubatorState(player) } override fun save(root: JSONObject) { - if(ticksLeft <= 0) return + if(pulse == null) return val data = JSONObject() data.put("eggOrdinal",(egg?.ordinal ?: 0).toString()) - data.put("ticksLeft",ticksLeft.toString()) + data.put("endTime", completionTimeMs.toString()) root.put("eggdata",data) } @@ -29,7 +34,11 @@ class IncubatorState(player: Player? = null) : State(player) { if(_data.containsKey("eggdata")){ val data = _data["eggdata"] as JSONObject egg = IncubatorEgg.values()[data["eggOrdinal"].toString().toInt()] - ticksLeft = data["ticksLeft"].toString().toInt() + + if (data.containsKey("ticksLeft")) + completionTimeMs = System.currentTimeMillis() + ticksToSeconds(data["ticksLeft"].toString().toInt() * 1000) + else + completionTimeMs = data["endTime"].toString().toLong() } } @@ -39,8 +48,8 @@ class IncubatorState(player: Player? = null) : State(player) { setVarbit(player, 4277, 1) pulse = object : Pulse(){ override fun pulse(): Boolean { - if(ticksLeft-- <= 0){ - player.setAttribute("/save:inc", egg!!.ordinal) + if(System.currentTimeMillis() >= completionTimeMs){ + player.setAttribute(ATTR_INCUBATOR_PRODUCT, egg!!.ordinal) player.sendMessage("Your " + egg!!.product.name.toLowerCase() + " has finished hatching.") pulse = null return true @@ -49,5 +58,8 @@ class IncubatorState(player: Player? = null) : State(player) { } } } - + + companion object { + const val ATTR_INCUBATOR_PRODUCT = "/save:incubator:egg-product" + } } diff --git a/Server/src/main/content/minigame/mta/TelekineticGrabSpell.java b/Server/src/main/content/minigame/mta/TelekineticGrabSpell.java index 7e1ac3530..9cb25850c 100644 --- a/Server/src/main/content/minigame/mta/TelekineticGrabSpell.java +++ b/Server/src/main/content/minigame/mta/TelekineticGrabSpell.java @@ -96,6 +96,8 @@ public final class TelekineticGrabSpell extends MagicSpell { @Override public boolean cast(final Entity entity, final Node target) { + if (!(target instanceof GroundItem)) + return false; final GroundItem ground = (GroundItem) target; if (!canCast(entity, ground)) { return false; diff --git a/Server/src/main/content/minigame/pyramidplunder/PlunderUtils.kt b/Server/src/main/content/minigame/pyramidplunder/PlunderUtils.kt index ac25834f2..28fbb6400 100644 --- a/Server/src/main/content/minigame/pyramidplunder/PlunderUtils.kt +++ b/Server/src/main/content/minigame/pyramidplunder/PlunderUtils.kt @@ -171,6 +171,8 @@ object PlunderUtils { fun getDoor(player: Player) : Int { + if (getRoom(player) == null) + return -1; val room = getRoom(player)!!.room return PlunderData.doors[room - 1] } @@ -219,6 +221,10 @@ object PlunderUtils { fun getChestXp(player: Player): Double { + if (getRoom(player) == null) { + expel(player, false) + return 0.0 + } val room = getRoom(player)!!.room return when(room) { diff --git a/Server/src/main/content/minigame/pyramidplunder/PyramidPlunderMinigame.kt b/Server/src/main/content/minigame/pyramidplunder/PyramidPlunderMinigame.kt index e2c7ba0d4..7f8d72b37 100644 --- a/Server/src/main/content/minigame/pyramidplunder/PyramidPlunderMinigame.kt +++ b/Server/src/main/content/minigame/pyramidplunder/PyramidPlunderMinigame.kt @@ -188,6 +188,10 @@ class PyramidPlunderMinigame : InteractionListener, TickListener, LogoutListener } on(SARCOPHAGUS, IntType.SCENERY, "open") { player, node -> + if (PlunderUtils.getRoom(player) == null) { + PlunderUtils.expel(player, false) + return@on true + } sendMessage(player, "You attempt to push open the massive lid.") val strength = getDynLevel(player, Skills.STRENGTH) animate(player, PUSH_LID_START) @@ -198,6 +202,10 @@ class PyramidPlunderMinigame : InteractionListener, TickListener, LogoutListener if(RandomFunction.random(125) > strength) return false animate(player, PUSH_LID_FINISH) + if (PlunderUtils.getRoom(player) == null) { + PlunderUtils.expel(player, false) + return true + } runTask(player, 3){ setVarbit(player, node.asScenery().definition.varbitID, 1) rewardXP(player, Skills.STRENGTH, PlunderUtils.getSarcophagusXp(player)) @@ -220,6 +228,10 @@ class PyramidPlunderMinigame : InteractionListener, TickListener, LogoutListener } on(Scenery.GRAND_GOLD_CHEST_16473, IntType.SCENERY, "search") { player, node -> + if (PlunderUtils.getRoom(player) == null) { + PlunderUtils.expel(player, false) + return@on true + } animate(player, OPEN_CHEST_ANIM) runTask(player){ if(RandomFunction.roll(25)) @@ -268,8 +280,14 @@ class PyramidPlunderMinigame : InteractionListener, TickListener, LogoutListener if(RandomFunction.roll(rate)) { val varbitId = node.asScenery().definition.varbitID - rewardXP(player, Skills.THIEVING, PlunderUtils.getDoorXp(player, rate == 3)) - if(PlunderUtils.getDoor(player) == varbitId) { + val door = PlunderUtils.getDoor(player) + + if (door == -1) + PlunderUtils.expel(player, false) + else + rewardXP(player, Skills.THIEVING, PlunderUtils.getDoorXp(player, rate == 3)) + + if (door == varbitId) { PlunderUtils.loadNextRoom(player) PlunderUtils.resetObjectVarbits(player) } else { diff --git a/Server/src/main/content/minigame/vinesweeper/VinesweeperDialogues.kt b/Server/src/main/content/minigame/vinesweeper/VinesweeperDialogues.kt index f51a18bde..23c012487 100644 --- a/Server/src/main/content/minigame/vinesweeper/VinesweeperDialogues.kt +++ b/Server/src/main/content/minigame/vinesweeper/VinesweeperDialogues.kt @@ -4,6 +4,7 @@ import core.game.node.item.Item import org.rs09.consts.Items import core.game.dialogue.DialogueFile import core.tools.END_DIALOGUE +import core.api.* abstract class FarmerDialogue : DialogueFile() { @@ -129,10 +130,11 @@ class BlinkinDialogue : FarmerDialogue() { 40 -> playerl("Do you have any Ogleroots to feed the rabbits?").also { stage++ } 41 -> npcl("I sure do. They'll cost ya 10 gold each. Any ya leave with will be returned to us, but ya'll get yer money back for 'em. How many do ya want?").also { stage++ } 42 -> { - player!!.setAttribute("runscript") { amount: Int -> + sendInputDialogue(player!!, InputType.AMOUNT, "Enter an amount:") { value -> + val amount = value as Int val price = Item(Items.COINS_995, 10 * amount) if(price.amount <= 0){ - return@setAttribute + return@sendInputDialogue } if(player!!.inventory.containsItem(price) && player!!.inventory.remove(price)) { if(player!!.inventory.add(Item(Items.OGLEROOT_12624, amount))) { diff --git a/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java b/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java index 5a4e59557..cff2ae212 100644 --- a/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java +++ b/Server/src/main/content/region/desert/handlers/KalphiteQueenNPC.java @@ -315,7 +315,7 @@ public final class KalphiteQueenNPC extends AbstractNPC { return; } if (style == CombatStyle.MAGIC) { - BattleState[] states = new BattleState[entity.getViewport().getCurrentPlane().getPlayers().size()]; + BattleState[] states = new BattleState[entity.getViewport().getCurrentPlane().getPlayers().size() + 1]; int index = 1; if (states.length == 0) { return; diff --git a/Server/src/main/content/region/desert/quest/thetouristrap/BedabinNomadDialogue.java b/Server/src/main/content/region/desert/quest/thetouristrap/BedabinNomadDialogue.java index e4a1b05d6..8cdabc88d 100644 --- a/Server/src/main/content/region/desert/quest/thetouristrap/BedabinNomadDialogue.java +++ b/Server/src/main/content/region/desert/quest/thetouristrap/BedabinNomadDialogue.java @@ -94,7 +94,8 @@ public final class BedabinNomadDialogue extends DialoguePlugin { case 2: end(); final Scenery door = RegionManager.getObject(new Location(3169, 3046, 0)); - SceneryBuilder.replace(door, door.transform(2701), 2); + if (door.getId() != 2701) + SceneryBuilder.replace(door, door.transform(2701), 2); player.getWalkingQueue().reset(); player.getWalkingQueue().addPath(3169, 3046); player.getPacketDispatch().sendMessage("You walk into the tent."); diff --git a/Server/src/main/content/region/kandarin/ardougne/quest/arena/cutscenes/EscapeCutscene.kt b/Server/src/main/content/region/kandarin/ardougne/quest/arena/cutscenes/EscapeCutscene.kt index 39caceb0c..fe4ead691 100644 --- a/Server/src/main/content/region/kandarin/ardougne/quest/arena/cutscenes/EscapeCutscene.kt +++ b/Server/src/main/content/region/kandarin/ardougne/quest/arena/cutscenes/EscapeCutscene.kt @@ -27,7 +27,7 @@ class EscapeCutscene(player: Player) : Cutscene(player) { } 1 -> { - player.faceLocation(location(2616, 3167, 0)) + player.faceLocation(location(56, 31, 0)) animate(player, 2098) timedUpdate(4) } @@ -38,8 +38,8 @@ class EscapeCutscene(player: Player) : Cutscene(player) { } 3 -> { - DoorActionHandler.handleAutowalkDoor(Jeremy, getScenery(2617, 3167, 0)) - player.faceLocation(location(2617, 3164, 0)) + DoorActionHandler.handleAutowalkDoor(Jeremy, getObject(57, 31, 0)) + player.faceLocation(location(57, 28, 0)) timedUpdate(3) } @@ -67,7 +67,7 @@ class EscapeCutscene(player: Player) : Cutscene(player) { moveCamera(47, 20) rotateCamera(45, 15) teleport(player, 47, 15) - Jeremy.teleport(Location.create(2616, 3167, 0)) + Jeremy.teleport(location(56, 31, 0)) timedUpdate(2) } diff --git a/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPlugin.java b/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPlugin.java index 691ee67f3..c66f8e3ae 100644 --- a/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPlugin.java +++ b/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPlugin.java @@ -12,6 +12,9 @@ import core.game.node.entity.npc.NPC; import core.game.node.entity.player.Player; import core.plugin.Plugin; import core.plugin.ClassScanner; +import kotlin.Unit; + +import static core.api.ContentAPIKt.*; /** * Handles the bogrog npc. @@ -46,8 +49,10 @@ public final class BogrogPlugin extends OptionHandler { player.sendMessage("You need a Summoning level of at least 21 in order to do that."); return; } else { - InterfaceContainer.generateItems(player,player.getInventory().toArray(),new String[] {"Swap X","Swap 10","Swap 5","Swap 1","Value"}, 644,0,7,4,200); - player.getInterfaceManager().openSingleTab(new Component(644)); + sendItemSelect(player, new String[] { "Value", "Swap 1", "Swap 5", "Swap 10", "Swap X" }, true, (slot, index) -> { + BogrogPouchSwapper.handle(player, index, slot); + return Unit.INSTANCE; + }); } } diff --git a/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPouchSwapper.kt b/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPouchSwapper.kt index 8af239407..02481d4c5 100644 --- a/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPouchSwapper.kt +++ b/Server/src/main/content/region/kandarin/feldip/gutanoth/handlers/BogrogPouchSwapper.kt @@ -17,11 +17,11 @@ import kotlin.math.floor */ object BogrogPouchSwapper { //Opcodes for item options - private const val OP_VALUE = 155 - private const val OP_SWAP_1 = 196 - private const val OP_SWAP_5 = 124 - private const val OP_SWAP_10 = 199 - private const val OP_SWAP_X = 234 + private const val OP_VALUE = 0 + private const val OP_SWAP_1 = 1 + private const val OP_SWAP_5 = 2 + private const val OP_SWAP_10 = 3 + private const val OP_SWAP_X = 4 private const val SPIRIT_SHARD = 12183 @@ -29,16 +29,16 @@ object BogrogPouchSwapper { private val GEBorders = ZoneBorders(3151,3501,3175,3477) @JvmStatic - fun handle(player: Player, opcode: Int, slot: Int, itemID: Int): Boolean{ - if(GEBorders.insideBorder(player)) return false - return when(opcode){ - OP_VALUE -> sendValue(player.inventory.get(slot).id,player) - OP_SWAP_1 -> swap(player, 1, player.inventory.get(slot).id) - OP_SWAP_5 -> swap(player, 5, player.inventory.get(slot).id) - OP_SWAP_10 -> swap(player,10,player.inventory.get(slot).id) + fun handle(player: Player, optionIndex: Int, slot: Int): Boolean{ + val item = player.inventory.get(slot) ?: return false + return when(optionIndex){ + OP_VALUE -> sendValue(item.id,player) + OP_SWAP_1 -> swap(player, 1, item.id) + OP_SWAP_5 -> swap(player, 5, item.id) + OP_SWAP_10 -> swap(player, 10, item.id) OP_SWAP_X -> true.also{ - sendInputDialogue(player, true, "Enter the amount:"){value -> - swap(player, value as Int, player.inventory.get(slot).id) + sendInputDialogue(player, InputType.AMOUNT, "Enter the amount:"){value -> + swap(player, value as Int, item.id) } } else -> false @@ -56,7 +56,6 @@ object BogrogPouchSwapper { amt = inInventory player.inventory.remove(Item(itemID,amt)) player.inventory.add(Item(SPIRIT_SHARD, floor(value * amt).toInt())) - player.interfaceManager.close(Component(644)) return true } @@ -66,7 +65,7 @@ object BogrogPouchSwapper { return false } - player.sendMessage("Bogrog will give you $value shards for that.") + player.sendMessage("Bogrog will give you ${floor(value).toInt()} shards for that.") return true } @@ -80,4 +79,4 @@ object BogrogPouchSwapper { if(isScroll) shardQuantity /= 20.0 return shardQuantity } -} \ No newline at end of file +} diff --git a/Server/src/main/content/region/kandarin/quest/fishingcontest/StairInteraction.java b/Server/src/main/content/region/kandarin/quest/fishingcontest/StairInteraction.java index 60b103359..c4ca0001b 100644 --- a/Server/src/main/content/region/kandarin/quest/fishingcontest/StairInteraction.java +++ b/Server/src/main/content/region/kandarin/quest/fishingcontest/StairInteraction.java @@ -10,6 +10,7 @@ import core.plugin.Initializable; import core.plugin.Plugin; import core.game.interaction.PluginInteraction; import core.game.interaction.PluginInteractionManager; +import core.game.world.repository.Repository; @Initializable public class StairInteraction extends PluginInteraction { @@ -33,7 +34,12 @@ public class StairInteraction extends PluginInteraction { player.getPulseManager().run(new MovementPulse(player,object.getLocation().transform(0,2,0)) { @Override public boolean pulse() { - player.getDialogueInterpreter().open(npc_id,new NPC(npc_id)); + NPC n = Repository.findNPC(npc_id); + if (n == null) { + player.sendMessage("Are you in a world without NPCs? What did you do?"); + return true; + } + player.getDialogueInterpreter().open(npc_id, n); return true; } }, PulseType.STANDARD); diff --git a/Server/src/main/content/region/misthalin/varrock/handlers/ZaffPlugin.kt b/Server/src/main/content/region/misthalin/varrock/handlers/ZaffPlugin.kt index 64abda94f..09a6f5c4e 100644 --- a/Server/src/main/content/region/misthalin/varrock/handlers/ZaffPlugin.kt +++ b/Server/src/main/content/region/misthalin/varrock/handlers/ZaffPlugin.kt @@ -387,7 +387,7 @@ class ZaffPlugin : OptionHandler() { ) stage = 1 } - 1 -> end().also { sendInputDialogue(player, InputType.NUMERIC, "Enter an amount:"){ value -> + 1 -> end().also { sendInputDialogue(player, InputType.AMOUNT, "Enter an amount:"){ value -> ammount = getStoreFile().getInt(player.username.toLowerCase()) var amt = value as Int if(amt > maxStaffs - ammount) amt = maxStaffs - ammount diff --git a/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java b/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java index ffd6a0778..ea1849876 100644 --- a/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java +++ b/Server/src/main/content/region/wilderness/handlers/CorporealBeastNPC.java @@ -163,6 +163,8 @@ public final class CorporealBeastNPC extends NPCBehavior { GameWorld.getPulser().submit(new Pulse(2, corp) { @Override public boolean pulse() { + if (npc.darkEnergyCore == null) + return true; npc.darkEnergyCore.init(); return true; } diff --git a/Server/src/main/core/api/ContentAPI.kt b/Server/src/main/core/api/ContentAPI.kt index a7de58500..912822b8a 100644 --- a/Server/src/main/core/api/ContentAPI.kt +++ b/Server/src/main/core/api/ContentAPI.kt @@ -1543,6 +1543,7 @@ fun sendInputDialogue(player: Player, type: InputType, prompt: String, handler: } player.setAttribute("runscript", handler) + player.setAttribute("input-type", type) } /** @@ -1728,9 +1729,11 @@ fun runcs2 (player: Player, scriptId: Int, vararg arguments: Any) { * Opens a generic item selection prompt with a glowing background, with your own callback to handle the selection. * @param player the player we are openinig the prompt for * @param options the right-click options the items should have + * @param keepAlive whether or not the selection prompt should remain open for multiple interactions * @param callback a callback to handle the selection. The parameters passed to the callback are the slot in the inventory of the selected item, and the 0-9 index of the option clicked. **/ -fun sendItemSelect (player: Player, vararg options: String, callback: (slot: Int, optionIndex: Int) -> Unit) { +@JvmOverloads +fun sendItemSelect (player: Player, vararg options: String, keepAlive: Boolean = false, callback: (slot: Int, optionIndex: Int) -> Unit) { player.interfaceManager.openSingleTab(Component(12)) val scriptArgs = arrayOf ((12 shl 16) + 18, 93, 4, 7, 0, -1, "", "", "", "", "", "", "", "", "") for (i in 0 until kotlin.math.min(9, options.size)) @@ -1741,6 +1744,7 @@ fun sendItemSelect (player: Player, vararg options: String, callback: (slot: Int .build() player.packetDispatch.sendIfaceSettings(settings, 18, 12, 0, 28) setAttribute(player, "itemselect-callback", callback) + setAttribute(player, "itemselect-keepalive", keepAlive) } fun announceIfRare(player: Player, item: Item) { @@ -2545,14 +2549,22 @@ fun logWithStack(origin: Class<*>, type: Log, message: String) { try { throw Exception(message) } catch (e: Exception) { - val sw = StringWriter() - val pw = PrintWriter(sw) - e.printStackTrace(pw) - - log(origin, type, "$sw") + log(origin, type, "${exceptionToString(e)}") } } +/** + * Takes an exception as an argument, and sends back the complete exception with stack trace as a standard string. Useful for various things. + * @param e The exception you wish to convert. + * @return a string with the full stack trace of the exception +**/ +fun exceptionToString (e: Exception) : String { + val sw = StringWriter() + val pw = PrintWriter(sw) + e.printStackTrace(pw) + return sw.toString() +} + /** * Used by content handlers to check if the entity is done moving yet */ diff --git a/Server/src/main/core/api/utils/Exceptions.kt b/Server/src/main/core/api/utils/Exceptions.kt new file mode 100644 index 000000000..4dd71a813 --- /dev/null +++ b/Server/src/main/core/api/utils/Exceptions.kt @@ -0,0 +1,3 @@ +package core.api.utils + +class ConfigParseException (message: String) : Exception (message) diff --git a/Server/src/main/core/api/utils/WeightBasedTable.kt b/Server/src/main/core/api/utils/WeightBasedTable.kt index cd4eb3ca9..c3a0acc50 100644 --- a/Server/src/main/core/api/utils/WeightBasedTable.kt +++ b/Server/src/main/core/api/utils/WeightBasedTable.kt @@ -27,10 +27,26 @@ open class WeightBasedTable : ArrayList() { guaranteedItems.add(element) } else { totalWeight += element.weight + val randIndex = RandomFunction.random(0, size) + val end = this.size super.add(element) + + val temp = this[randIndex] + this[randIndex] = element + this[end] = temp + true } } + fun roll(receiver: Entity? = null, rollCount: Int) : ArrayList { + val items = ArrayList() + + for (i in 0 until rollCount) + items.addAll(roll(receiver)) + + return items + } + open fun roll(receiver: Entity? = null): ArrayList{ val items = ArrayList() items.addAll(guaranteedItems) @@ -40,7 +56,7 @@ open class WeightBasedTable : ArrayList() { } else if (isNotEmpty()) { var rngWeight = RandomFunction.randomDouble(totalWeight) - for (item in shuffled()) { + for (item in this) { rngWeight -= item.weight if (rngWeight <= 0) { items.add(item) @@ -179,4 +195,4 @@ open class WeightBasedTable : ArrayList() { } return builder.toString() } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/dialogue/DialogueFile.kt b/Server/src/main/core/game/dialogue/DialogueFile.kt index 37d3cea88..f81911e31 100644 --- a/Server/src/main/core/game/dialogue/DialogueFile.kt +++ b/Server/src/main/core/game/dialogue/DialogueFile.kt @@ -20,6 +20,7 @@ abstract class DialogueFile { this.player = player this.npc = npc this.interpreter = interpreter + interpreter.activeTopics.clear() return this } diff --git a/Server/src/main/core/game/dialogue/DialogueInterpreter.java b/Server/src/main/core/game/dialogue/DialogueInterpreter.java index 4974a39ad..67e285e8b 100644 --- a/Server/src/main/core/game/dialogue/DialogueInterpreter.java +++ b/Server/src/main/core/game/dialogue/DialogueInterpreter.java @@ -142,8 +142,9 @@ public final class DialogueInterpreter { close(); return; } + DialogueFile file = dialogue.file; - if (!activeTopics.isEmpty()) { + if (!activeTopics.isEmpty() && buttonId >= 2) { Topic topic = activeTopics.get(buttonId - 2); if (!topic.getSkipPlayer()) @@ -573,4 +574,4 @@ public final class DialogueInterpreter { public List getActions() { return actions; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/dialogue/DialoguePlugin.java b/Server/src/main/core/game/dialogue/DialoguePlugin.java index e4967e2e9..635b41c2b 100644 --- a/Server/src/main/core/game/dialogue/DialoguePlugin.java +++ b/Server/src/main/core/game/dialogue/DialoguePlugin.java @@ -194,6 +194,7 @@ public abstract class DialoguePlugin implements Plugin { * @return {@code True} if the dialogue plugin succesfully opened. */ public boolean open(Object... args) { + player.getDialogueInterpreter().activeTopics.clear(); if (args.length > 0 && args[0] instanceof NPC) { npc = (NPC)args[0]; } diff --git a/Server/src/main/core/game/dialogue/SimpleEntityMessage.java b/Server/src/main/core/game/dialogue/SimpleEntityMessage.java deleted file mode 100644 index cfc718e91..000000000 --- a/Server/src/main/core/game/dialogue/SimpleEntityMessage.java +++ /dev/null @@ -1,46 +0,0 @@ -package core.game.dialogue; - -import core.plugin.Initializable; -import core.game.node.entity.player.Player; - -/** - * Handles the SimpleEntityMessage dialogue. - * @author 'Vexia - */ -@Initializable -public class SimpleEntityMessage extends DialoguePlugin { - - public SimpleEntityMessage() { - - } - - public SimpleEntityMessage(Player player) { - super(player); - } - - @Override - public int[] getIds() { - return new int[] { 8000 }; - } - - @Override - public boolean handle(int interfaceId, int buttonId) { - end(); - return true; - } - - @Override - public DialoguePlugin newInstance(Player player) { - - return new SimpleEntityMessage(player); - } - - @Override - public boolean open(Object... args) { - String[] messages = new String[args.length]; - for (int i = 0; i < messages.length; i++) - messages[i] = (String) args[i]; - interpreter.sendDialogues(player, FacialExpression.HALF_GUILTY, messages); - return true; - } -} diff --git a/Server/src/main/core/game/global/report/AbuseReport.java b/Server/src/main/core/game/global/report/AbuseReport.java index 0a47d41f1..dee08a06f 100644 --- a/Server/src/main/core/game/global/report/AbuseReport.java +++ b/Server/src/main/core/game/global/report/AbuseReport.java @@ -51,7 +51,7 @@ public final class AbuseReport { CommandMapping.INSTANCE.get("mute").attemptHandling(player, new String[] {"mute", victim, "48h"}); } player.getPacketDispatch().sendMessage("Thank-you, your abuse report has been received."); - Discord.INSTANCE.postPlayerAlert(victim, "Abuse Report - " + rule.name()); + Discord.postPlayerAlert(victim, "Abuse Report - " + rule.name()); } /** diff --git a/Server/src/main/core/game/interaction/MovementPulse.java b/Server/src/main/core/game/interaction/MovementPulse.java index fb4adbfe2..628695b44 100644 --- a/Server/src/main/core/game/interaction/MovementPulse.java +++ b/Server/src/main/core/game/interaction/MovementPulse.java @@ -267,7 +267,8 @@ public abstract class MovementPulse extends Pulse { } else if(overrideMethod != null){ loc = overrideMethod.invoke(mover,destination); - if(loc == destination.getLocation()) loc = destinationFlag.getDestination(mover,destination); + if(loc == destination.getLocation() && destinationFlag != null) loc = destinationFlag.getDestination(mover,destination); + else if (loc == destination.getLocation()) loc = null; } if (loc == null) { diff --git a/Server/src/main/core/game/node/Node.java b/Server/src/main/core/game/node/Node.java index 4310044d2..fd20d6cda 100644 --- a/Server/src/main/core/game/node/Node.java +++ b/Server/src/main/core/game/node/Node.java @@ -197,6 +197,8 @@ public abstract class Node { * @param direction The direction to set. */ public void setDirection(Direction direction) { + if (direction == null) + return; this.direction = direction; } diff --git a/Server/src/main/core/game/node/entity/npc/drop/NPCDropTables.java b/Server/src/main/core/game/node/entity/npc/drop/NPCDropTables.java index f133408db..4a676150d 100644 --- a/Server/src/main/core/game/node/entity/npc/drop/NPCDropTables.java +++ b/Server/src/main/core/game/node/entity/npc/drop/NPCDropTables.java @@ -42,7 +42,7 @@ public final class NPCDropTables { */ public static final int[] MESSAGE_NPCS = { 50, 7133, 7134, 2881, 2882, 2883, 3200, 3340, 6247, 6203, 6260, 6222, 2745, 1160, 8133, 8610, 8611, 8612, 8613, 8614, 6204, 6206, 6208, 6261, 6263, 6265, 6223, 6225, 6227 }; - public final NPCDropTable table = new NPCDropTable(); + public NPCDropTable table = new NPCDropTable(); /** * The NPC definitions. diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index 78055a720..15b6e9e82 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -317,6 +317,8 @@ public class Player extends Entity { public byte[] opCounts = new byte[255]; + public int invalidPacketCount = 0; + /** * Constructs a new {@code Player} {@code Object}. * @param details The player's details. @@ -1386,4 +1388,12 @@ public class Player extends Entity { public void updateAppearance() { getUpdateMasks().register(EntityFlag.Appearance, this); } + + public void incrementInvalidPacketCount() { + invalidPacketCount++; + if (invalidPacketCount >= 5) { + clear(); + log(this.getClass(), Log.ERR, "Disconnecting " + getName() + " for having a high rate of invalid packets. Potential packet bot misbehaving, or simply really bad connection."); + } + } } diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt index db9e3d1f0..7127babf3 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt @@ -643,6 +643,8 @@ class PlayerSaver (val player: Player){ val varpData = JSONArray() for ((index, value) in player.varpMap) { + if (!(player.saveVarp[index] ?: false)) continue + val varpObj = JSONObject() varpObj["index"] = index.toString() varpObj["value"] = value.toString() diff --git a/Server/src/main/core/game/node/entity/player/link/InterfaceManager.java b/Server/src/main/core/game/node/entity/player/link/InterfaceManager.java index a1699352f..dcf8fb7db 100644 --- a/Server/src/main/core/game/node/entity/player/link/InterfaceManager.java +++ b/Server/src/main/core/game/node/entity/player/link/InterfaceManager.java @@ -570,7 +570,7 @@ public final class InterfaceManager { */ public void setViewedTab(int tabIndex) { if (tabs[tabIndex] == null) { - throw new IllegalStateException("Tab at index " + tabIndex + " is null!"); + return; } currentTabIndex = tabIndex; switch (tabIndex) { diff --git a/Server/src/main/core/game/node/entity/state/StateRepository.kt b/Server/src/main/core/game/node/entity/state/StateRepository.kt index 9f6419153..2cc7fc3b3 100644 --- a/Server/src/main/core/game/node/entity/state/StateRepository.kt +++ b/Server/src/main/core/game/node/entity/state/StateRepository.kt @@ -27,7 +27,7 @@ class StateRepository : StartupListener{ @JvmStatic fun forKey(key: String, player: Player): State?{ val state = states[key] - if(player.states[key] != null){ + if(player.hasActiveState(key)){ return null } if(state != null){ @@ -38,4 +38,4 @@ class StateRepository : StartupListener{ return null } } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/node/scenery/SceneryBuilder.java b/Server/src/main/core/game/node/scenery/SceneryBuilder.java index 63b7bd8ed..3dac14bf0 100644 --- a/Server/src/main/core/game/node/scenery/SceneryBuilder.java +++ b/Server/src/main/core/game/node/scenery/SceneryBuilder.java @@ -46,7 +46,6 @@ public final class SceneryBuilder { remove = remove.getWrapper(); Scenery current = LandscapeParser.removeScenery(remove); if (current == null) { - log(SceneryBuilder.class, Log.ERR, "Object could not be replaced with " + construct + " - object to remove is invalid."); return false; } if (current.getRestorePulse() != null) { @@ -119,7 +118,6 @@ public final class SceneryBuilder { remove = remove.getWrapper(); Scenery current = LandscapeParser.removeScenery(remove); if (current == null) { - log(SceneryBuilder.class, Log.ERR, "Object could not be replaced with " + construct + " - object to remove is invalid."); return false; } if (current.getRestorePulse() != null) { diff --git a/Server/src/main/core/game/shops/Shop.kt b/Server/src/main/core/game/shops/Shop.kt index 3a22e35b0..ac2bb7b2b 100644 --- a/Server/src/main/core/game/shops/Shop.kt +++ b/Server/src/main/core/game/shops/Shop.kt @@ -158,6 +158,12 @@ class Shop(val title: String, val stock: Array, val general: Boolean = val isMainStock = getAttribute(player, "shop-main", true) val cont = if (isMainStock) getAttribute(player, "shop-cont", null) ?: return Item(-1,-1) else playerStock val item = cont[slot] + + if (item == null) { + player.sendMessage("That item doesn't appear to be there anymore. Please try again.") + return Item(-1, -1) + } + val price = when(currency) { Items.TOKKUL_6529 -> item.definition.getConfiguration(ItemConfigParser.TOKKUL_PRICE, 1) @@ -173,6 +179,13 @@ class Shop(val title: String, val stock: Array, val general: Boolean = { val shopCont = getAttribute(player, "shop-cont", null) ?: return Pair(null, Item(-1,-1)) val item = player.inventory[slot] + + if (item == null) { + player.debug("Inventory slot $slot does not contain an item!") + player.sendMessage("That item doesn't seem to be there anymore. Please try again.") + return Pair(null, Item(-1,-1)) + } + val shopItemId = if (item.definition.isUnnoted) { item.id } diff --git a/Server/src/main/core/game/shops/Shops.kt b/Server/src/main/core/game/shops/Shops.kt index 52a8de020..3b5697306 100644 --- a/Server/src/main/core/game/shops/Shops.kt +++ b/Server/src/main/core/game/shops/Shops.kt @@ -184,7 +184,7 @@ class Shops : StartupListener, TickListener, InteractionListener, InterfaceListe OP_BUY_1 -> shop.buy(player, slot, 1) OP_BUY_5 -> shop.buy(player, slot, 5) OP_BUY_10 -> shop.buy(player, slot, 10) - OP_BUY_X -> sendInputDialogue(player, true, "Enter the amount to buy:"){value -> + OP_BUY_X -> sendInputDialogue(player, InputType.AMOUNT, "Enter the amount to buy:"){value -> val amt = value as Int shop.buy(player, slot, amt) } @@ -226,6 +226,12 @@ class Shops : StartupListener, TickListener, InteractionListener, InterfaceListe val shop = getAttribute(player, "shop", null) ?: return@on false + val itemInSlot = player.inventory[slot] + if (itemInSlot == null) { + player.sendMessage("That item doesn't appear to be there anymore. Please try again.") + return@on true + } + val (_,price) = shop.getSellPrice(player, slot) val def = itemDefinition(player.inventory[slot].id) @@ -243,7 +249,7 @@ class Shops : StartupListener, TickListener, InteractionListener, InterfaceListe OP_SELL_1 -> shop.sell(player, slot, 1) OP_SELL_5 -> shop.sell(player, slot, 5) OP_SELL_10 -> shop.sell(player, slot, 10) - OP_SELL_X -> sendInputDialogue(player, true, "Enter the amount to sell:"){value -> + OP_SELL_X -> sendInputDialogue(player, InputType.AMOUNT, "Enter the amount to sell:"){value -> val amt = value as Int shop.sell(player, slot, amt) } 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 5532c7d72..c193f5c64 100644 --- a/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt @@ -258,6 +258,10 @@ class MiscCommandSet : CommandSet(Privilege.ADMIN){ reject(player, "Usage: ::commands page") } + if (page > pages.size) { + reject(player, "That page number is too high, you don't have access to that many.") + } + for (i in 0..310) { player.packetDispatch.sendString("", Components.QUESTJOURNAL_SCROLL_275, i) } diff --git a/Server/src/main/core/game/system/command/sets/MusicCommandSet.kt b/Server/src/main/core/game/system/command/sets/MusicCommandSet.kt index 09e99cf03..def057ecb 100644 --- a/Server/src/main/core/game/system/command/sets/MusicCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/MusicCommandSet.kt @@ -46,6 +46,8 @@ class MusicCommandSet : CommandSet(Privilege.STANDARD){ * music player yet. */ define("playid"){player,arg -> + if (arg.size < 2) + reject(player, "Needs more args.") val id = arg[1].toIntOrNull() if(id != null){ PacketRepository.send(MusicPacket::class.java, MusicContext(player, id)) @@ -63,4 +65,4 @@ class MusicCommandSet : CommandSet(Privilege.STANDARD){ } } } -} \ No newline at end of file +} diff --git a/Server/src/main/core/game/system/communication/CommunicationInfo.java b/Server/src/main/core/game/system/communication/CommunicationInfo.java index ea146fec3..895d45865 100644 --- a/Server/src/main/core/game/system/communication/CommunicationInfo.java +++ b/Server/src/main/core/game/system/communication/CommunicationInfo.java @@ -257,6 +257,10 @@ public final class CommunicationInfo { */ public static void add(Player player, String contact) { CommunicationInfo info = player.getDetails().getCommunication(); + if (contact.isEmpty()) { + player.sendMessage("Unable to find a player by that name. Please try again."); + return; + } if (WorldCommunicator.isEnabled()) { MSPacketRepository.sendContactUpdate(player.getName(), contact, false, false, null); return; @@ -291,6 +295,10 @@ public final class CommunicationInfo { * @param block If the contact should be removed from the block list. */ public static void remove(Player player, String contact, boolean block) { + if (contact.isEmpty()) { + player.sendMessage("Unable to find a player by that name. Please try again."); + return; + } if (WorldCommunicator.isEnabled()) { MSPacketRepository.sendContactUpdate(player.getName(), contact, true, block, null); return; @@ -326,6 +334,10 @@ public final class CommunicationInfo { * @param contact The contact to block. */ public static void block(Player player, String contact) { + if (contact.isEmpty()) { + player.sendMessage("Unable to find a player by that name. Please try again."); + return; + } if (WorldCommunicator.isEnabled()) { MSPacketRepository.sendContactUpdate(player.getName(), contact, false, true, null); return; @@ -352,6 +364,10 @@ public final class CommunicationInfo { * @param clanRank The clan rank to set. */ public static void updateClanRank(Player player, String contact, ClanRank clanRank) { + if (contact.isEmpty()) { + player.sendMessage("Unable to find a player by that name. Please try again."); + return; + } CommunicationInfo info = player.getDetails().getCommunication(); Contact c = info.contacts.get(contact); if (c == null) { diff --git a/Server/src/main/core/game/system/config/DropTableParser.kt b/Server/src/main/core/game/system/config/DropTableParser.kt index 4dc3b6793..30c1b6e2f 100644 --- a/Server/src/main/core/game/system/config/DropTableParser.kt +++ b/Server/src/main/core/game/system/config/DropTableParser.kt @@ -5,13 +5,13 @@ import org.json.simple.JSONArray import org.json.simple.JSONObject import org.json.simple.parser.JSONParser import core.ServerConstants -import core.api.log -import core.api.utils.NPCDropTable -import core.api.utils.WeightedItem +import core.api.* +import core.api.utils.* import core.tools.Log import core.tools.SystemLogger import java.io.FileReader + class DropTableParser { val parser = JSONParser() var reader: FileReader? = null @@ -22,13 +22,21 @@ class DropTableParser { for(i in obj){ val tab = i as JSONObject val ids = tab["ids"].toString().split(",") - for(n in ids){ - val def = NPCDefinition.forId(n.toInt()).dropTables - def ?: continue - parseTable(tab["main"] as JSONArray, def.table, false) - parseTable(tab["charm"] as JSONArray, def.table, false, true) - parseTable(tab["default"] as JSONArray,def.table,true) - count++ + + try { + val table = NPCDropTable() + parseTable(tab["main"] as JSONArray, table, false) + parseTable(tab["charm"] as JSONArray, table, false, true) + parseTable(tab["default"] as JSONArray, table, true) + + for(n in ids){ + val def = NPCDefinition.forId(n.toInt()).dropTables + def ?: continue + def.table = table + count++ + } + } catch (e: ConfigParseException) { + log (this::class.java, Log.ERR, "Error parsing drop tables for NPC ${NPCDefinition.forId(ids[0].toInt()).name}: ${exceptionToString(e)}") } } log(this::class.java, Log.FINE, "Parsed $count drop tables.") @@ -40,10 +48,15 @@ class DropTableParser { val id = item["id"].toString().toInt() val minAmount = item["minAmount"].toString().toInt() val maxAmount = item["maxAmount"].toString().toInt() + + if (minAmount > maxAmount) { + throw ConfigParseException("Table is invalid! Specified minimum amount is > specified maximum amount.") + } + val weight = item["weight"].toString().toDouble() val newItem = WeightedItem(id,minAmount,maxAmount,weight.toDouble(),isAlways) if(isCharms) destTable.addToCharms(newItem) else destTable.add(newItem) } } -} \ No newline at end of file +} diff --git a/Server/src/main/core/integrations/discord/Discord.kt b/Server/src/main/core/integrations/discord/Discord.kt index 77f841521..acb6755f7 100644 --- a/Server/src/main/core/integrations/discord/Discord.kt +++ b/Server/src/main/core/integrations/discord/Discord.kt @@ -1,6 +1,6 @@ package core.integrations.discord -import core.api.getItemName +import core.api.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.json.simple.JSONArray @@ -11,159 +11,193 @@ import java.io.DataOutputStream import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.URL +import java.util.ArrayList -object Discord { - private const val COLOR_NEW_BUY_OFFER = 47789 - private const val COLOR_NEW_SALE_OFFER = 5752709 - private const val COLOR_OFFER_UPDATE = 15588691 +class Discord : TickListener { + override fun tick() { + if (!removeList.isEmpty()) { + pendingMessages.removeAll(removeList) + removeList.clear() + } + if (!pendingMessages.isEmpty() && !flushingPends) { + flushingPends = true + GlobalScope.launch { + try { + val count = pendingMessages.size + for (i in 0 until count) { + val (url, content) = pendingMessages[i] + sendJsonPost(url, content) + removeList.add(pendingMessages[i]) + } + } finally { + flushingPends = false + } + } + } + } - fun postNewOffer(isSale: Boolean, itemId: Int, value: Int, qty: Int, user: String) { - if (ServerConstants.DISCORD_GE_WEBHOOK.isEmpty()) return - GlobalScope.launch { - val offer = encodeOfferJson(isSale, itemId, value, qty, user) + companion object { + private const val COLOR_NEW_BUY_OFFER = 47789 + private const val COLOR_NEW_SALE_OFFER = 5752709 + private const val COLOR_OFFER_UPDATE = 15588691 + + private val pendingMessages = ArrayList>() + private var flushingPends = false + private val removeList = ArrayList>() + + fun postNewOffer(isSale: Boolean, itemId: Int, value: Int, qty: Int, user: String) { + if (ServerConstants.DISCORD_GE_WEBHOOK.isEmpty()) return + GlobalScope.launch { + val offer = encodeOfferJson(isSale, itemId, value, qty, user) + try { + sendJsonPost(ServerConstants.DISCORD_GE_WEBHOOK, offer) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + fun postOfferUpdate(isSale: Boolean, itemId: Int, value: Int, amtLeft: Int) { + if (ServerConstants.DISCORD_GE_WEBHOOK.isEmpty()) return + GlobalScope.launch { + val offer = encodeUpdateJson(isSale, itemId, value, amtLeft) + try { + sendJsonPost(ServerConstants.DISCORD_GE_WEBHOOK, offer) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + @JvmStatic + fun postPlayerAlert(player: String, type: String) { + if (ServerConstants.DISCORD_MOD_WEBHOOK.isEmpty()) return + GlobalScope.launch { + val alert = encodeUserAlert(type, player) + try { + sendJsonPost(ServerConstants.DISCORD_MOD_WEBHOOK, alert) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + @JvmStatic fun sendToOpenRSC(player: String, type: String) { + if (ServerConstants.DISCORD_OPENRSC_HOOK.isEmpty()) return + GlobalScope.launch { + val alert = encodeUserAlert(type, player) + try { + sendJsonPost(ServerConstants.DISCORD_OPENRSC_HOOK, alert) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun encodeUpdateJson(sale: Boolean, itemId: Int, value: Int, amtLeft: Int): String { + val obj = JSONObject() + val embeds = JSONArray() + val embed = JSONObject() + + val fields = arrayOf( + EmbedField("Item", getItemName(itemId), false), + EmbedField("Amount Remaining", "%,d".format(amtLeft), true), + EmbedField("Price", "%,d".format(value) + "gp", true), + ) + + embed["title"] = if (sale) "Sell Offer Updated" else "Buy Offer Updated" + embed["color"] = COLOR_OFFER_UPDATE + embed["thumbnail"] = getItemImage(itemId) + embed["fields"] = getFields(fields) + + embeds.add(embed) + obj["embeds"] = embeds + + return obj.toJSONString() + } + + private fun encodeOfferJson(isSale: Boolean, itemId: Int, value: Int, qty: Int, user: String): String { + val obj = JSONObject() + val embeds = JSONArray() + val embed = JSONObject() + + val fields = arrayOf( + EmbedField("Player", user, false), + EmbedField("Item", getItemName(itemId), false), + EmbedField("Amount", "%,d".format(qty), true), + EmbedField("Price", "%,d".format(value) + "gp", true), + ) + + embed["title"] = if (isSale) "New Sell Offer" else "New Buy Offer" + embed["color"] = if (isSale) COLOR_NEW_SALE_OFFER else COLOR_NEW_BUY_OFFER + embed["thumbnail"] = getItemImage(itemId) + embed["fields"] = getFields(fields) + + embeds.add(embed) + obj["embeds"] = embeds + + return obj.toJSONString() + } + + private fun encodeUserAlert(type: String, player: String) : String { + val obj = JSONObject() + val embeds = JSONArray() + val embed = JSONObject() + + val fields = arrayOf( + EmbedField("Player", player, false), + EmbedField("Type", type, false) + ) + + embed["title"] = "Player Alert" + embed["fields"] = getFields(fields) + embeds.add(embed) + obj["embeds"] = embeds + return obj.toJSONString() + } + + private fun getFields(fields: Array): JSONArray { + val arr = JSONArray() + + for (field in fields) { + val o = JSONObject() + o["name"] = field.name + o["value"] = field.value + if (field.inline) o["inline"] = true + arr.add(o) + } + + return arr + } + + data class EmbedField(val name: String, val value: String, val inline: Boolean) + + fun getItemImage(id: Int) : JSONObject { + val obj = JSONObject() + obj["url"] = "https://github.com/2009scape/2009scape.github.io/raw/master/services/m%3Ddata/img/items/$id.png" + return obj + } + + private fun sendJsonPost(url: String = ServerConstants.DISCORD_GE_WEBHOOK, data: String) { try { - sendJsonPost(ServerConstants.DISCORD_GE_WEBHOOK, offer) + val conn = URL(url).openConnection() as HttpURLConnection + conn.doOutput = true + conn.requestMethod = "POST" + conn.setRequestProperty("Content-Type", "application/json") + conn.useCaches = false + + DataOutputStream(conn.outputStream).use { it.writeBytes(data) } + BufferedReader(InputStreamReader(conn.inputStream)).use { br -> + var line: String? + while (br.readLine().also { line = it } != null) { + println(line) + } + } } catch (e: Exception) { - e.printStackTrace() + pendingMessages.add(Pair(url, data)) } } } - - fun postOfferUpdate(isSale: Boolean, itemId: Int, value: Int, amtLeft: Int) { - if (ServerConstants.DISCORD_GE_WEBHOOK.isEmpty()) return - GlobalScope.launch { - val offer = encodeUpdateJson(isSale, itemId, value, amtLeft) - try { - sendJsonPost(ServerConstants.DISCORD_GE_WEBHOOK, offer) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - fun postPlayerAlert(player: String, type: String) { - if (ServerConstants.DISCORD_MOD_WEBHOOK.isEmpty()) return - GlobalScope.launch { - val alert = encodeUserAlert(type, player) - try { - sendJsonPost(ServerConstants.DISCORD_MOD_WEBHOOK, alert) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - @JvmStatic fun sendToOpenRSC(player: String, type: String) { - if (ServerConstants.DISCORD_OPENRSC_HOOK.isEmpty()) return - GlobalScope.launch { - val alert = encodeUserAlert(type, player) - try { - sendJsonPost(ServerConstants.DISCORD_OPENRSC_HOOK, alert) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - private fun encodeUpdateJson(sale: Boolean, itemId: Int, value: Int, amtLeft: Int): String { - val obj = JSONObject() - val embeds = JSONArray() - val embed = JSONObject() - - val fields = arrayOf( - EmbedField("Item", getItemName(itemId), false), - EmbedField("Amount Remaining", "%,d".format(amtLeft), true), - EmbedField("Price", "%,d".format(value) + "gp", true), - ) - - embed["title"] = if (sale) "Sell Offer Updated" else "Buy Offer Updated" - embed["color"] = COLOR_OFFER_UPDATE - embed["thumbnail"] = getItemImage(itemId) - embed["fields"] = getFields(fields) - - embeds.add(embed) - obj["embeds"] = embeds - - return obj.toJSONString() - } - - private fun encodeOfferJson(isSale: Boolean, itemId: Int, value: Int, qty: Int, user: String): String { - val obj = JSONObject() - val embeds = JSONArray() - val embed = JSONObject() - - val fields = arrayOf( - EmbedField("Player", user, false), - EmbedField("Item", getItemName(itemId), false), - EmbedField("Amount", "%,d".format(qty), true), - EmbedField("Price", "%,d".format(value) + "gp", true), - ) - - embed["title"] = if (isSale) "New Sell Offer" else "New Buy Offer" - embed["color"] = if (isSale) COLOR_NEW_SALE_OFFER else COLOR_NEW_BUY_OFFER - embed["thumbnail"] = getItemImage(itemId) - embed["fields"] = getFields(fields) - - embeds.add(embed) - obj["embeds"] = embeds - - return obj.toJSONString() - } - - private fun encodeUserAlert(type: String, player: String) : String { - val obj = JSONObject() - val embeds = JSONArray() - val embed = JSONObject() - - val fields = arrayOf( - EmbedField("Player", player, false), - EmbedField("Type", type, false) - ) - - embed["title"] = "Player Alert" - embed["fields"] = getFields(fields) - embeds.add(embed) - obj["embeds"] = embeds - return obj.toJSONString() - } - - private fun getFields(fields: Array): JSONArray { - val arr = JSONArray() - - for (field in fields) { - val o = JSONObject() - o["name"] = field.name - o["value"] = field.value - if (field.inline) o["inline"] = true - arr.add(o) - } - - return arr - } - - data class EmbedField(val name: String, val value: String, val inline: Boolean) - - fun getItemImage(id: Int) : JSONObject { - val obj = JSONObject() - obj["url"] = "https://github.com/2009scape/2009scape.github.io/raw/master/services/m%3Ddata/img/items/$id.png" - return obj - } - - private fun sendJsonPost(url: String = ServerConstants.DISCORD_GE_WEBHOOK, data: String) { - val conn = URL(url).openConnection() as HttpURLConnection - conn.doOutput = true - conn.requestMethod = "POST" - conn.setRequestProperty("Content-Type", "application/json") - conn.useCaches = false - - DataOutputStream(conn.outputStream).use { it.writeBytes(data) } - BufferedReader(InputStreamReader(conn.inputStream)).use { br -> - var line: String? - while (br.readLine().also { line = it } != null) { - println(line) - } - } - } -} \ No newline at end of file +} diff --git a/Server/src/main/core/net/amsc/MSPacketRepository.java b/Server/src/main/core/net/amsc/MSPacketRepository.java index c8ea619be..f1072e6fd 100644 --- a/Server/src/main/core/net/amsc/MSPacketRepository.java +++ b/Server/src/main/core/net/amsc/MSPacketRepository.java @@ -92,6 +92,9 @@ public final class MSPacketRepository { buffer.put(publicSetting); buffer.put(privateSetting); buffer.put(tradeSetting); - WorldCommunicator.getSession().write(buffer); + if (WorldCommunicator.getSession() != null) + WorldCommunicator.getSession().write(buffer); + else + player.sendMessage("Privacy settings unavailable at the moment. Please try again later."); } -} \ No newline at end of file +} diff --git a/Server/src/main/core/net/event/GameReadEvent.java b/Server/src/main/core/net/event/GameReadEvent.java index 0e5c061c4..f8dc8a948 100644 --- a/Server/src/main/core/net/event/GameReadEvent.java +++ b/Server/src/main/core/net/event/GameReadEvent.java @@ -74,6 +74,7 @@ public final class GameReadEvent extends IoReadEvent { size = getPacketSize(buffer, opcode, header, last); } if (size == -1) { + session.getPlayer().incrementInvalidPacketCount(); break; } if (buffer.remaining() < size) { @@ -136,11 +137,7 @@ public final class GameReadEvent extends IoReadEvent { } return buffer.getShort() & 0xFFFF; } - if(header == -3){ - - } - log(this.getClass(), Log.ERR, "Invalid packet [opcode=" + opcode + ", last=" + last + ", queued=" + usedQueuedBuffer + "], header=" + header+"!"); return -1; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/net/event/MSReadEvent.java b/Server/src/main/core/net/event/MSReadEvent.java index 5c1edfe13..5e12d6963 100644 --- a/Server/src/main/core/net/event/MSReadEvent.java +++ b/Server/src/main/core/net/event/MSReadEvent.java @@ -88,8 +88,7 @@ public final class MSReadEvent extends IoReadEvent { } return buffer.getShort() & 0xFFFF; } - log(this.getClass(), Log.ERR, "Invalid packet [opcode=" + opcode + ", last=" + last + ", queued=" + usedQueuedBuffer + "]!"); return -1; } -} \ No newline at end of file +} diff --git a/Server/src/main/core/net/packet/PacketProcessor.kt b/Server/src/main/core/net/packet/PacketProcessor.kt index e1d151e67..c4b877e1f 100644 --- a/Server/src/main/core/net/packet/PacketProcessor.kt +++ b/Server/src/main/core/net/packet/PacketProcessor.kt @@ -158,6 +158,10 @@ object PacketProcessor { } } is Packet.ReportAbuse -> { + if (pkt.target.isNullOrEmpty()) { + sendMessage(pkt.player, "Invalid player name.") + return + } if (!GameWorld.accountStorage.checkUsernameTaken(pkt.target.lowercase())) { sendMessage(pkt.player, "Invalid player name.") return @@ -571,9 +575,9 @@ object PacketProcessor { player = pkt.player } else if (pkt is Packet.UseWithGroundItem) { val container = getLikelyContainerForIface(pkt.player, pkt.iface) ?: return - item = container[pkt.slot] + item = container[pkt.slot] ?: return itemId = pkt.usedId - node = GroundItemManager.get(pkt.withId, Location.create(pkt.x, pkt.y, pkt.player.location.z), pkt.player) + node = GroundItemManager.get(pkt.withId, Location.create(pkt.x, pkt.y, pkt.player.location.z), pkt.player) ?: return nodeId = pkt.withId type = IntType.GROUNDITEM player = pkt.player diff --git a/Server/src/main/core/net/packet/in/Decoders530.kt b/Server/src/main/core/net/packet/in/Decoders530.kt index b6ae35734..70fcc6e89 100644 --- a/Server/src/main/core/net/packet/in/Decoders530.kt +++ b/Server/src/main/core/net/packet/in/Decoders530.kt @@ -9,6 +9,7 @@ import core.tools.StringUtils import core.tools.SystemLogger import java.io.PrintWriter import java.io.StringWriter +import java.nio.BufferUnderflowException enum class Decoders530(val opcode: Int) { /****************************************** @@ -635,8 +636,13 @@ enum class Decoders530(val opcode: Int) { COMMAND(44) { override fun decode(player: Player, buffer: IoBuffer): Packet { if (buffer.toByteBuffer().remaining() > 1) { - val message = buffer.string.lowercase() - return Packet.Command(player, message) + try { + val message = buffer.string.lowercase() + return Packet.Command(player, message) + } catch (e: BufferUnderflowException) { + player.sendMessage("Something went wrong there! Please try again.") + return Packet.NoProcess() + } } return Packet.NoProcess() } @@ -795,4 +801,4 @@ enum class Decoders530(val opcode: Int) { } } } -} \ No newline at end of file +} diff --git a/Server/src/main/core/net/packet/in/Login.kt b/Server/src/main/core/net/packet/in/Login.kt index c635f41c4..02f89076b 100644 --- a/Server/src/main/core/net/packet/in/Login.kt +++ b/Server/src/main/core/net/packet/in/Login.kt @@ -89,6 +89,9 @@ object Login { } return Pair(AuthResponse.Success, info) + } catch (buf: BufferUnderflowException) { + //some issue in either the data they sent us or how we read it, either way out of scope of this class's handling. + return Pair(AuthResponse.UnexpectedError, null) } catch (e: Exception) { log(this::class.java, Log.ERR, "Exception encountered during login packet parsing! See stack trace below.") e.printStackTrace() diff --git a/Server/src/main/core/net/packet/in/RunScript.kt b/Server/src/main/core/net/packet/in/RunScript.kt index 48b88504b..b3f900714 100644 --- a/Server/src/main/core/net/packet/in/RunScript.kt +++ b/Server/src/main/core/net/packet/in/RunScript.kt @@ -1,6 +1,6 @@ package core.net.packet.`in` -import core.api.sendDialogue +import core.api.* import core.game.node.entity.player.Player /** @@ -12,6 +12,8 @@ object RunScript { fun processInput(player: Player, value: Any, script: ((Any) -> Boolean)) { if (value is Int && value <= 0) return + + val type = player.getAttribute("input-type", InputType.NUMERIC) var input = value @@ -26,10 +28,18 @@ object RunScript { input = input.replace("k", "000").replace("m", "000000") } + if (type == InputType.NUMERIC || type == InputType.AMOUNT) + input = input.toString().toIntOrNull() ?: input + try { script(input) } catch (_: NumberFormatException) { sendDialogue(player, "That number's a bit large, don't you think?") + } catch (_: ClassCastException) { + sendDialogue(player, "Something went wrong here. Try again.") + } finally { + removeAttribute(player, "runscript") + removeAttribute(player, "input-type") } } } diff --git a/Server/src/main/core/tools/StringUtils.java b/Server/src/main/core/tools/StringUtils.java index b0a941986..84ac0e3e0 100644 --- a/Server/src/main/core/tools/StringUtils.java +++ b/Server/src/main/core/tools/StringUtils.java @@ -409,6 +409,7 @@ public final class StringUtils { * @return The string. */ public static String longToString(long l) { + try { int i = 0; char ac[] = new char[32]; while (l != 0L) { @@ -417,6 +418,9 @@ public final class StringUtils { ac[11 - i++] = VALID_CHARS[(int) (l1 - l * 37L)]; } return new String(ac, 12 - i, i); + } catch (ArrayIndexOutOfBoundsException e) { + return ""; + } } /**