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!") + } + } + +}