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
This commit is contained in:
Player Name 2024-05-31 11:32:48 +00:00 committed by Ryan
parent 2776270c7f
commit 49a10d192a
14 changed files with 132 additions and 28 deletions

View file

@ -11,7 +11,7 @@ import org.rs09.consts.NPCs
object DrillDemonUtils { object DrillDemonUtils {
val DD_KEY_TASK = "/save:drilldemon:task" 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_VARP = 531
val DD_SIGN_JOG = 0 val DD_SIGN_JOG = 0
val DD_SIGN_SITUP = 1 val DD_SIGN_SITUP = 1

View file

@ -13,7 +13,7 @@ import org.rs09.consts.NPCs
import org.rs09.consts.Scenery import org.rs09.consts.Scenery
object EvilBobUtils { object EvilBobUtils {
const val prevLocation = "/save:evilbob:prevlocation" const val prevLocation = "/save:original-loc"
const val eventComplete = "/save:evilbob:eventcomplete" const val eventComplete = "/save:evilbob:eventcomplete"
const val assignedFishingZone = "/save:evilbob:fishingzone" const val assignedFishingZone = "/save:evilbob:fishingzone"
const val fishCaught = "evilbob:fishcaught" const val fishCaught = "evilbob:fishcaught"

View file

@ -10,7 +10,7 @@ import core.tools.RandomFunction
object FreakUtils{ object FreakUtils{
const val freakNpc = NPCs.FREAKY_FORESTER_2458 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 freakTask = "/save:freakyf:task"
const val freakComplete = "/save:freakyf:complete" const val freakComplete = "/save:freakyf:complete"
const val pheasantKilled = "freakyf:killed" const val pheasantKilled = "freakyf:killed"

View file

@ -13,7 +13,7 @@ import core.ServerConstants
object SurpriseExamUtils { object SurpriseExamUtils {
val SE_KEY_LOC = "supexam:loc" val SE_KEY_LOC = "/save:original-loc"
val SE_KEY_INDEX = "supexam:index" val SE_KEY_INDEX = "supexam:index"
val SE_LOGOUT_KEY = "suprise_exam" val SE_LOGOUT_KEY = "suprise_exam"
val SE_DOOR_KEY = "supexam:door" val SE_DOOR_KEY = "supexam:door"

View file

@ -8,6 +8,8 @@ import core.game.node.item.GroundItemManager;
import core.game.node.item.Item; import core.game.node.item.Item;
import core.tools.RandomFunction; import core.tools.RandomFunction;
import static core.api.ContentAPIKt.addItemOrBank;
/** /**
* Represents the gertrudes fortress quest. * Represents the gertrudes fortress quest.
* @author 'Vexia * @author 'Vexia
@ -84,7 +86,7 @@ public class GertrudesCat extends Quest {
player.getPacketDispatch().sendItemZoomOnInterface(kitten.getId(), 240, 277, 3 + 2); player.getPacketDispatch().sendItemZoomOnInterface(kitten.getId(), 240, 277, 3 + 2);
setStage(player, 100); setStage(player, 100);
if (player.getFamiliarManager().hasFamiliar()) { if (player.getFamiliarManager().hasFamiliar()) {
player.getInventory().add(kitten); addItemOrBank(player, kitten.getId(), 1);
} else { } else {
player.getFamiliarManager().summon(kitten, true, false); player.getFamiliarManager().summon(kitten, true, false);
} }

View file

@ -17,6 +17,9 @@ class ServerConstants {
companion object { companion object {
var NOAUTH_DEFAULT_ADMIN: Boolean = true var NOAUTH_DEFAULT_ADMIN: Boolean = true
@JvmField
var CURRENT_SAVEFILE_VERSION = 1
@JvmField @JvmField
var DAILY_ACCOUNT_LIMIT = 3 var DAILY_ACCOUNT_LIMIT = 3

View file

@ -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. * Clears an NPC with the "poof" smoke graphics commonly seen with random event NPCs.
* @param npc the NPC object to initialize * @param npc the NPC object to initialize

View file

@ -49,7 +49,7 @@ class Grave : AbstractNPC {
this.ownerUid = player.details.uid this.ownerUid = player.details.uid
this.ownerUsername = player.username this.ownerUsername = player.username
this.location = location this.location = player.getAttribute("/save:original-loc",location)
this.isRespawn = false this.isRespawn = false
this.isWalks = false this.isWalks = false
this.isNeverWalks = true this.isNeverWalks = true

View file

@ -40,7 +40,7 @@ class GraveController : PersistWorld, TickListener, InteractionListener, Command
player.details.rights = Rights.REGULAR_PLAYER player.details.rights = Rights.REGULAR_PLAYER
setAttribute(player, "tutorial:complete", true) setAttribute(player, "tutorial:complete", true)
player.impactHandler.manualHit(player, player.skills.lifepoints, ImpactHandler.HitsplatType.NORMAL) 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) { GameWorld.Pulser.submit(object : Pulse(15) {
override fun pulse(): Boolean { override fun pulse(): Boolean {
player.details.rights = Rights.ADMINISTRATOR player.details.rights = Rights.ADMINISTRATOR

View file

@ -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_BASE;
import static core.game.system.command.sets.StatAttributeKeysKt.STATS_DEATHS; import static core.game.system.command.sets.StatAttributeKeysKt.STATS_DEATHS;
import static core.tools.GlobalsKt.colorize; import static core.tools.GlobalsKt.colorize;
import static org.rs09.consts.Items.BONES_526;
/** /**
* Represents a player entity. * 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. * The amount of targets that the player can shoot left for the archery minigame.
*/ */
private int archeryTargets = 0; private int archeryTargets = 0;
private int archeryTotal = 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; public int invalidPacketCount = 0;
/** /**
@ -611,7 +619,7 @@ public class Player extends Entity {
if (this.isArtificial() && killer instanceof NPC) { if (this.isArtificial() && killer instanceof NPC) {
return; 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); 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())); 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()); killer.setAttribute("/save:last-murder-news", getWorldTicks());
@ -630,7 +638,7 @@ public class Player extends Entity {
return; 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); final Container[] c = DeathTask.getContainers(this);
for (Item i : getEquipment().toArray()) { for (Item i : getEquipment().toArray()) {
@ -684,6 +692,10 @@ public class Player extends Entity {
skullManager.setSkulled(false); skullManager.setSkulled(false);
removeAttribute("combat-time"); removeAttribute("combat-time");
getPrayer().reset(); 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); super.finalizeDeath(killer);
appearance.sync(); appearance.sync();
if (!getSavedData().getGlobalData().isDeathScreenDisabled()) { if (!getSavedData().getGlobalData().isDeathScreenDisabled()) {

View file

@ -152,18 +152,8 @@ public final class LoginConfiguration {
if (item == null) continue; if (item == null) continue;
player.getEquipment().remove(item); player.getEquipment().remove(item);
if (!InteractionListeners.run(item.getId(), player, item, true) || !player.getEquipment().add(item, true, false)) { 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."));
player.sendMessage (colorize("%RAs you can no longer wear " + item.getName() + ", it has been unequipped.")); addItemOrBank(player, item.getId(), item.getAmount());
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);
}
} }
} }

View file

@ -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 } reader ?: log(this::class.java, Log.WARN, "Couldn't find save file for ${player.name}, or save is corrupted.").also { read = false }
if (read) { if (read) {
saveFile = parser.parse(reader) as JSONObject saveFile = parser.parse(reader) as JSONObject
}
if (read) {
parseData() parseData()
} }
} }
@ -80,7 +77,7 @@ class PlayerSaveParser(val player: Player) {
parseStatistics() parseStatistics()
parseAchievements() parseAchievements()
parsePouches() parsePouches()
parsePouches() parseVersion()
} }
fun runContentHooks() fun runContentHooks()
@ -377,5 +374,11 @@ class PlayerSaveParser(val player: Player) {
player.settings.parse(settingsData) player.settings.parse(settingsData)
} }
fun parseVersion() {
saveFile ?: return
player.version = 0
if (saveFile!!.containsKey("version")) {
player.version = saveFile!!["version"].toString().toInt()
}
}
} }

View file

@ -56,6 +56,7 @@ class PlayerSaver (val player: Player){
saveStatManager(saveFile) saveStatManager(saveFile)
saveAttributes(saveFile) saveAttributes(saveFile)
savePouches(saveFile) savePouches(saveFile)
saveVersion(saveFile)
contentHooks.forEach { it.savePlayer(player, saveFile) } contentHooks.forEach { it.savePlayer(player, saveFile) }
return saveFile return saveFile
} }
@ -96,6 +97,10 @@ class PlayerSaver (val player: Player){
player.pouchManager.save(root) player.pouchManager.save(root)
} }
fun saveVersion(root: JSONObject){
root.put("version", player.version)
}
fun saveAttributes(root: JSONObject){ fun saveAttributes(root: JSONObject){
if(player.gameAttributes.savedAttributes.isNotEmpty()){ if(player.gameAttributes.savedAttributes.isNotEmpty()){
val attrs = JSONArray() val attrs = JSONArray()

View file

@ -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, "<col=CC6600>Migrating save file version ${player.version} to current save file version ${ServerConstants.CURRENT_SAVEFILE_VERSION}.</col>")
// 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, "<col=CC6600>You are being given $need crafting hood(s), because we think you bought $need crafting cape(s) when the hoods were still unobtainable.</col>")
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, "<col=CC6600>Save file migration complete. Happy scaping!</col>")
}
}
}