From 49a10d192abcac0b823a3e6145d1021a0178f978 Mon Sep 17 00:00:00 2001 From: Player Name Date: Fri, 31 May 2024 11:32:48 +0000 Subject: [PATCH] Implemented save file versioning Players who likely bought their crafting capes back when the hood was not obtainable will be given the complementary hood Unlocked Surok's Theme for players who are eligible Fixed some bugs relating to the handling of edge cases for random events Fixed tutorial island quest completion Made it possible to collapse interface stones in resizable HD --- .../ame/events/drilldemon/DrillDemonUtils.kt | 2 +- .../global/ame/events/evilbob/EvilBobUtils.kt | 2 +- .../ame/events/freakyforester/FreakUtils.kt | 2 +- .../events/supriseexam/SurpriseExamUtils.kt | 2 +- .../varrock/quest/gertrude/GertrudesCat.java | 4 +- Server/src/main/core/ServerConstants.kt | 3 + Server/src/main/core/api/ContentAPI.kt | 20 ++++++ .../game/node/entity/combat/graves/Grave.kt | 2 +- .../entity/combat/graves/GraveController.kt | 2 +- .../core/game/node/entity/player/Player.java | 20 ++++-- .../player/info/login/LoginConfiguration.java | 14 +--- .../player/info/login/PlayerSaveParser.kt | 13 ++-- .../entity/player/info/login/PlayerSaver.kt | 5 ++ .../player/info/login/SaveVersionHooks.kt | 69 +++++++++++++++++++ 14 files changed, 132 insertions(+), 28 deletions(-) create mode 100644 Server/src/main/core/game/node/entity/player/info/login/SaveVersionHooks.kt diff --git a/Server/src/main/content/global/ame/events/drilldemon/DrillDemonUtils.kt b/Server/src/main/content/global/ame/events/drilldemon/DrillDemonUtils.kt index a85bde6a3..d81f3ed51 100644 --- a/Server/src/main/content/global/ame/events/drilldemon/DrillDemonUtils.kt +++ b/Server/src/main/content/global/ame/events/drilldemon/DrillDemonUtils.kt @@ -11,7 +11,7 @@ import org.rs09.consts.NPCs object DrillDemonUtils { val DD_KEY_TASK = "/save:drilldemon:task" - val DD_KEY_RETURN_LOC = "/save:drilldemon:original-loc" + val DD_KEY_RETURN_LOC = "/save:original-loc" val DD_SIGN_VARP = 531 val DD_SIGN_JOG = 0 val DD_SIGN_SITUP = 1 diff --git a/Server/src/main/content/global/ame/events/evilbob/EvilBobUtils.kt b/Server/src/main/content/global/ame/events/evilbob/EvilBobUtils.kt index 9d5f4957d..e07703ab9 100644 --- a/Server/src/main/content/global/ame/events/evilbob/EvilBobUtils.kt +++ b/Server/src/main/content/global/ame/events/evilbob/EvilBobUtils.kt @@ -13,7 +13,7 @@ import org.rs09.consts.NPCs import org.rs09.consts.Scenery object EvilBobUtils { - const val prevLocation = "/save:evilbob:prevlocation" + const val prevLocation = "/save:original-loc" const val eventComplete = "/save:evilbob:eventcomplete" const val assignedFishingZone = "/save:evilbob:fishingzone" const val fishCaught = "evilbob:fishcaught" diff --git a/Server/src/main/content/global/ame/events/freakyforester/FreakUtils.kt b/Server/src/main/content/global/ame/events/freakyforester/FreakUtils.kt index cd76dc23d..c08701297 100644 --- a/Server/src/main/content/global/ame/events/freakyforester/FreakUtils.kt +++ b/Server/src/main/content/global/ame/events/freakyforester/FreakUtils.kt @@ -10,7 +10,7 @@ import core.tools.RandomFunction object FreakUtils{ const val freakNpc = NPCs.FREAKY_FORESTER_2458 - const val freakPreviousLoc = "/save:freakyf:location" + const val freakPreviousLoc = "/save:original-loc" const val freakTask = "/save:freakyf:task" const val freakComplete = "/save:freakyf:complete" const val pheasantKilled = "freakyf:killed" diff --git a/Server/src/main/content/global/ame/events/supriseexam/SurpriseExamUtils.kt b/Server/src/main/content/global/ame/events/supriseexam/SurpriseExamUtils.kt index d3905d358..ca0d405f6 100644 --- a/Server/src/main/content/global/ame/events/supriseexam/SurpriseExamUtils.kt +++ b/Server/src/main/content/global/ame/events/supriseexam/SurpriseExamUtils.kt @@ -13,7 +13,7 @@ import core.ServerConstants object SurpriseExamUtils { - val SE_KEY_LOC = "supexam:loc" + val SE_KEY_LOC = "/save:original-loc" val SE_KEY_INDEX = "supexam:index" val SE_LOGOUT_KEY = "suprise_exam" val SE_DOOR_KEY = "supexam:door" diff --git a/Server/src/main/content/region/misthalin/varrock/quest/gertrude/GertrudesCat.java b/Server/src/main/content/region/misthalin/varrock/quest/gertrude/GertrudesCat.java index 3abadbd9c..16ce95fcd 100644 --- a/Server/src/main/content/region/misthalin/varrock/quest/gertrude/GertrudesCat.java +++ b/Server/src/main/content/region/misthalin/varrock/quest/gertrude/GertrudesCat.java @@ -8,6 +8,8 @@ import core.game.node.item.GroundItemManager; import core.game.node.item.Item; import core.tools.RandomFunction; +import static core.api.ContentAPIKt.addItemOrBank; + /** * Represents the gertrudes fortress quest. * @author 'Vexia @@ -84,7 +86,7 @@ public class GertrudesCat extends Quest { player.getPacketDispatch().sendItemZoomOnInterface(kitten.getId(), 240, 277, 3 + 2); setStage(player, 100); if (player.getFamiliarManager().hasFamiliar()) { - player.getInventory().add(kitten); + addItemOrBank(player, kitten.getId(), 1); } else { player.getFamiliarManager().summon(kitten, true, false); } diff --git a/Server/src/main/core/ServerConstants.kt b/Server/src/main/core/ServerConstants.kt index bad28925c..8673f4d93 100644 --- a/Server/src/main/core/ServerConstants.kt +++ b/Server/src/main/core/ServerConstants.kt @@ -17,6 +17,9 @@ class ServerConstants { companion object { var NOAUTH_DEFAULT_ADMIN: Boolean = true + @JvmField + var CURRENT_SAVEFILE_VERSION = 1 + @JvmField var DAILY_ACCOUNT_LIMIT = 3 diff --git a/Server/src/main/core/api/ContentAPI.kt b/Server/src/main/core/api/ContentAPI.kt index 8bd1ef83e..b86f81482 100644 --- a/Server/src/main/core/api/ContentAPI.kt +++ b/Server/src/main/core/api/ContentAPI.kt @@ -378,6 +378,26 @@ fun addItemOrDrop(player: Player, id: Int, amount: Int = 1) { } } +/** + * Add an item with a variable quantity or bank it if a player does not have enough space, or drop it if that still doesn't work + * @param player the player whose inventory to add to + * @param id the ID of the item to add to the player's inventory + * @param amount the amount of the ID to add to the player's inventory, defaults to 1 + */ +fun addItemOrBank(player: Player, id: Int, amount: Int = 1) { + val item = Item(id, amount) + if (!player.inventory.add(item)) { + if (player.bankPrimary.add(item)) { + sendMessage(player, colorize("%RThe ${item.name} has been sent to your bank.")) + } else if (player.bankSecondary.add(item)) { + sendMessage(player, colorize("%RThe ${item.name} has been sent to your secondary bank.")) + } else { + GroundItemManager.create(item, player) + sendMessage(player, colorize("%RAs your inventory and bank account(s) are all full, the ${item.name} has been placed on the ground under your feet. Don't forget to grab it. (Also consider cleaning out some stuff, maybe? I mean, Jesus!)")) + } + } +} + /** * Clears an NPC with the "poof" smoke graphics commonly seen with random event NPCs. * @param npc the NPC object to initialize diff --git a/Server/src/main/core/game/node/entity/combat/graves/Grave.kt b/Server/src/main/core/game/node/entity/combat/graves/Grave.kt index efd2f9b17..2118ab7dc 100644 --- a/Server/src/main/core/game/node/entity/combat/graves/Grave.kt +++ b/Server/src/main/core/game/node/entity/combat/graves/Grave.kt @@ -49,7 +49,7 @@ class Grave : AbstractNPC { this.ownerUid = player.details.uid this.ownerUsername = player.username - this.location = location + this.location = player.getAttribute("/save:original-loc",location) this.isRespawn = false this.isWalks = false this.isNeverWalks = true diff --git a/Server/src/main/core/game/node/entity/combat/graves/GraveController.kt b/Server/src/main/core/game/node/entity/combat/graves/GraveController.kt index a57a76562..98998d120 100644 --- a/Server/src/main/core/game/node/entity/combat/graves/GraveController.kt +++ b/Server/src/main/core/game/node/entity/combat/graves/GraveController.kt @@ -40,7 +40,7 @@ class GraveController : PersistWorld, TickListener, InteractionListener, Command player.details.rights = Rights.REGULAR_PLAYER setAttribute(player, "tutorial:complete", true) player.impactHandler.manualHit(player, player.skills.lifepoints, ImpactHandler.HitsplatType.NORMAL) - notify(player, "Grave created at ${player.location}") + notify(player, "Grave created at ${player.getAttribute("/save:original-loc",player.location)}") GameWorld.Pulser.submit(object : Pulse(15) { override fun pulse(): Boolean { player.details.rights = Rights.ADMINISTRATOR 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 d9fc9478b..361fa58c8 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -91,6 +91,7 @@ import static core.api.utils.Permadeath.PermadeathKt.permadeath; import static core.game.system.command.sets.StatAttributeKeysKt.STATS_BASE; import static core.game.system.command.sets.StatAttributeKeysKt.STATS_DEATHS; import static core.tools.GlobalsKt.colorize; +import static org.rs09.consts.Items.BONES_526; /** * Represents a player entity. @@ -310,11 +311,18 @@ public class Player extends Entity { * The amount of targets that the player can shoot left for the archery minigame. */ private int archeryTargets = 0; - private int archeryTotal = 0; - public byte[] opCounts = new byte[255]; + /** + * The save file version. + */ + public int version = ServerConstants.CURRENT_SAVEFILE_VERSION; + /** + * Packet administration. + * opCounts is used to enforce an authentic limit of 10 of each inbound packet per user per tick. + */ + public byte[] opCounts = new byte[255]; public int invalidPacketCount = 0; /** @@ -611,7 +619,7 @@ public class Player extends Entity { if (this.isArtificial() && killer instanceof NPC) { return; } - if (killer instanceof Player && getWorldTicks() - killer.getAttribute("/save:last-murder-news", 0) >= 500) { + if (killer instanceof Player && killer.getName() != getName() /* happens if you died via typeless damage from an external cause, e.g. bugs in a dark cave without a light source */ && getWorldTicks() - killer.getAttribute("/save:last-murder-news", 0) >= 500) { Item wep = getItemFromEquipment((Player) killer, EquipmentSlot.WEAPON); sendNews(killer.getUsername() + " has murdered " + getUsername() + " with " + (wep == null ? "their fists." : (StringUtils.isPlusN(wep.getName()) ? "an " : "a ") + wep.getName())); killer.setAttribute("/save:last-murder-news", getWorldTicks()); @@ -630,7 +638,7 @@ public class Player extends Entity { return; } } - GroundItemManager.create(new Item(526), getLocation(), k); + GroundItemManager.create(new Item(BONES_526), this.getAttribute("/save:original-loc",location), k); final Container[] c = DeathTask.getContainers(this); for (Item i : getEquipment().toArray()) { @@ -684,6 +692,10 @@ public class Player extends Entity { skullManager.setSkulled(false); removeAttribute("combat-time"); getPrayer().reset(); + removeAttribute("original-loc"); //in case you died inside a random event + interfaceManager.openDefaultTabs(); //in case you died inside a random that had blanked them + setComponentVisibility(this, 548, 69, false); //reenable the logout button (SD) + setComponentVisibility(this, 746, 12, false); //reenable the logout button (HD) super.finalizeDeath(killer); appearance.sync(); if (!getSavedData().getGlobalData().isDeathScreenDisabled()) { diff --git a/Server/src/main/core/game/node/entity/player/info/login/LoginConfiguration.java b/Server/src/main/core/game/node/entity/player/info/login/LoginConfiguration.java index 3406bee18..0217cdbc6 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/LoginConfiguration.java +++ b/Server/src/main/core/game/node/entity/player/info/login/LoginConfiguration.java @@ -152,18 +152,8 @@ public final class LoginConfiguration { if (item == null) continue; player.getEquipment().remove(item); if (!InteractionListeners.run(item.getId(), player, item, true) || !player.getEquipment().add(item, true, false)) { - if (player.getInventory().add(item)) - player.sendMessage (colorize("%RAs you can no longer wear " + item.getName() + ", it has been unequipped.")); - else if (player.getBankPrimary().add(item)) - player.sendMessage (colorize("%RAs you can no longer wear " + item.getName() + ", it has been sent to your bank.")); - else if (player.getBankSecondary().add(item)) - player.sendMessage (colorize("%RAs you can no longer wear " + item.getName() + ", it has been sent to your secondary bank.")); - else { - player.sendMessage (colorize("%RAs you can no longer wear " + item.getName() + ", and your inventory and both banks are full,")); - player.sendMessage (colorize("%RIt has been placed on the ground under your feet. Don't forget to grab it.")); - player.sendMessage ("(Also, consider cleaning out your banks maybe? I mean jesus.)"); - GroundItemManager.create (item, player); - } + player.sendMessage(colorize("%RAs you can no longer wear " + item.getName() + ", it has been unequipped.")); + addItemOrBank(player, item.getId(), item.getAmount()); } } diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt index 524811dc8..1d712aeef 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt @@ -48,9 +48,6 @@ class PlayerSaveParser(val player: Player) { reader ?: log(this::class.java, Log.WARN, "Couldn't find save file for ${player.name}, or save is corrupted.").also { read = false } if (read) { saveFile = parser.parse(reader) as JSONObject - } - - if (read) { parseData() } } @@ -80,7 +77,7 @@ class PlayerSaveParser(val player: Player) { parseStatistics() parseAchievements() parsePouches() - parsePouches() + parseVersion() } fun runContentHooks() @@ -377,5 +374,11 @@ class PlayerSaveParser(val player: Player) { player.settings.parse(settingsData) } - + fun parseVersion() { + saveFile ?: return + player.version = 0 + if (saveFile!!.containsKey("version")) { + player.version = saveFile!!["version"].toString().toInt() + } + } } 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 9b595082a..511f730a2 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 @@ -56,6 +56,7 @@ class PlayerSaver (val player: Player){ saveStatManager(saveFile) saveAttributes(saveFile) savePouches(saveFile) + saveVersion(saveFile) contentHooks.forEach { it.savePlayer(player, saveFile) } return saveFile } @@ -96,6 +97,10 @@ class PlayerSaver (val player: Player){ player.pouchManager.save(root) } + fun saveVersion(root: JSONObject){ + root.put("version", player.version) + } + fun saveAttributes(root: JSONObject){ if(player.gameAttributes.savedAttributes.isNotEmpty()){ val attrs = JSONArray() diff --git a/Server/src/main/core/game/node/entity/player/info/login/SaveVersionHooks.kt b/Server/src/main/core/game/node/entity/player/info/login/SaveVersionHooks.kt new file mode 100644 index 000000000..69de55908 --- /dev/null +++ b/Server/src/main/core/game/node/entity/player/info/login/SaveVersionHooks.kt @@ -0,0 +1,69 @@ +package core.game.node.entity.player.info.login + +import core.ServerConstants +import core.api.* +import core.game.node.entity.player.Player +import core.game.node.item.Item +import org.rs09.consts.Items + + +/** + * Runs one-time save-version-related hooks. + * @author Player Name + */ + +class SaveVersionHooks : LoginListener { + + override fun login(player: Player) { + if (player.version < ServerConstants.CURRENT_SAVEFILE_VERSION) { + sendMessage(player, "Migrating save file version ${player.version} to current save file version ${ServerConstants.CURRENT_SAVEFILE_VERSION}.") + + // Perform actual migrations + if (player.version < 1) { // GL #1811 + // Give out crafting hoods if the player bought any crafting capes when the hoods were not obtainable + var hasHoods = 0 + var hasCapes = 0 + val searchSpace = arrayOf(player.inventory, player.bankPrimary, player.bankSecondary) + for (container in searchSpace) { + for (hood in container.getAll(Item(Items.CRAFTING_HOOD_9782))) { + hasHoods += hood.amount + } + for (id in arrayOf(Items.CRAFTING_CAPE_9780, Items.CRAFTING_CAPET_9781)) { + for (cape in container.getAll(Item(id))) { + hasCapes += cape.amount + } + } + } + val need = hasCapes - hasHoods + if (need > 0) { + sendMessage(player, "You are being given $need crafting hood(s), because we think you bought $need crafting cape(s) when the hoods were still unobtainable.") + addItemOrBank(player, Items.CRAFTING_HOOD_9782, need) + } + + // Unlock Surok's Theme if eligible + if (getQuestStage(player, "What Lies Below") > 70) { + player.musicPlayer.unlock(250, false) + } + + // Migrate random-event saved location attributes to the uniform naming scheme + for (old in arrayOf("/save:drilldemon:original-loc","/save:evilbob:prevlocation","/save:freakyf:location","supexam:loc")) { + val oldloc = player.getAttribute(old, player.location) + if (oldloc != player.location) { + player.setAttribute("/save:original-loc", oldloc) + } + player.removeAttribute(old) + } + + // Set the missing tutorial island varp if eligible + if (getAttribute(player, "/save:tutorial:complete", false)) { + setVarp(player, 281, 1000, true) + } + } + + // Finish up + player.version = ServerConstants.CURRENT_SAVEFILE_VERSION + sendMessage(player, "Save file migration complete. Happy scaping!") + } + } + +}