diff --git a/Server/src/main/java/core/game/content/global/action/DropItemHandler.java b/Server/src/main/java/core/game/content/global/action/DropItemHandler.java index 5b6da8cc9..918d78fd8 100644 --- a/Server/src/main/java/core/game/content/global/action/DropItemHandler.java +++ b/Server/src/main/java/core/game/content/global/action/DropItemHandler.java @@ -6,6 +6,8 @@ import core.game.node.entity.player.info.login.PlayerParser; import core.game.node.entity.player.link.audio.Audio; import core.game.node.item.GroundItemManager; import core.game.node.item.Item; +import rs09.game.node.entity.player.graves.Grave; +import rs09.game.node.entity.player.graves.GraveController; import rs09.game.system.SystemLogger; import rs09.game.system.config.ItemConfigParser; import rs09.game.world.GameWorld; @@ -42,6 +44,10 @@ public final class DropItemHandler { player.getDialogueInterpreter().open(9878, item); return true; } + if (GraveController.hasGraveAt(player.getLocation())) { + player.sendMessage("You cannot drop items on top of graves!"); + return false; + } if (player.getAttribute("equipLock:" + item.getId(), 0) > GameWorld.getTicks()) { SystemLogger.logAlert(player + ", tried to do the drop & equip dupe."); return true; diff --git a/Server/src/main/java/core/game/content/quest/free/therestlessghost/FatherAereckDialogue.java b/Server/src/main/java/core/game/content/quest/free/therestlessghost/FatherAereckDialogue.java index 34083b405..481dced4a 100644 --- a/Server/src/main/java/core/game/content/quest/free/therestlessghost/FatherAereckDialogue.java +++ b/Server/src/main/java/core/game/content/quest/free/therestlessghost/FatherAereckDialogue.java @@ -237,8 +237,6 @@ public final class FatherAereckDialogue extends DialoguePlugin { case 10: end(); player.getInterfaceManager().open(new Component(652)); - BitregisterAssembler.send(player, 652, 34, 0, 13, new BitregisterAssembler(0)); - player.getConfigManager().set(1146, player.getGraveManager().getType().ordinal() | 262112); player.getAchievementDiaryManager().finishTask(player, DiaryType.LUMBRIDGE, 0, 15); break; case 20: diff --git a/Server/src/main/java/core/game/interaction/inter/GravePurchaseInterface.java b/Server/src/main/java/core/game/interaction/inter/GravePurchaseInterface.java deleted file mode 100644 index 7e9b88ed0..000000000 --- a/Server/src/main/java/core/game/interaction/inter/GravePurchaseInterface.java +++ /dev/null @@ -1,44 +0,0 @@ -package core.game.interaction.inter; - -import core.game.component.Component; -import core.game.component.ComponentDefinition; -import core.game.component.ComponentPlugin; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.link.grave.GraveType; -import core.game.node.item.Item; -import core.plugin.Initializable; -import core.plugin.Plugin; - -/** - * Represents the component plugin used for the grave purchasing interface. - * @author Vexia - */ -@Initializable -public final class GravePurchaseInterface extends ComponentPlugin { - - @Override - public Plugin newInstance(Object arg) throws Throwable { - ComponentDefinition.put(652, this); - return this; - } - - @Override - public boolean handle(Player player, Component component, int opcode, int button, int slot, int itemId) { - if (slot == -1) { - return true; - } - final GraveType grave = GraveType.values()[slot]; - int cost = grave.getCost(); - if (!player.getInventory().containsItem(new Item(995, cost))) { - return true; - } - if (!player.getInventory().remove(new Item(995, cost)) && grave != GraveType.MEMORIAL_PLAQUE) { - player.getPacketDispatch().sendMessage("You don't have enough coins to buy this grave stone."); - return true; - } - player.getGraveManager().setType(grave); - player.getDialogueInterpreter().sendDialogue("Your gravestone has been changed as you requested."); - player.getInterfaceManager().close(); - return true; - } -} diff --git a/Server/src/main/java/core/game/node/entity/npc/other/GraveStoneNPC.java b/Server/src/main/java/core/game/node/entity/npc/other/GraveStoneNPC.java deleted file mode 100644 index 463763a52..000000000 --- a/Server/src/main/java/core/game/node/entity/npc/other/GraveStoneNPC.java +++ /dev/null @@ -1,470 +0,0 @@ -package core.game.node.entity.npc.other; - -import java.util.List; - -import core.cache.def.impl.NPCDefinition; -import core.game.component.Component; -import core.game.node.entity.skill.Skills; -import core.game.interaction.OptionHandler; -import core.game.node.Node; -import core.game.node.entity.Entity; -import core.game.node.entity.combat.CombatStyle; -import core.game.node.entity.npc.AbstractNPC; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.link.HintIconManager; -import core.game.node.entity.player.link.grave.GraveManager; -import core.game.node.entity.player.link.grave.GraveType; -import core.game.node.item.GroundItem; -import rs09.game.world.GameWorld; -import core.game.world.map.Location; -import rs09.game.world.repository.Repository; -import core.game.world.update.flag.context.Animation; -import core.game.world.update.flag.context.Graphics; -import core.plugin.Plugin; -import core.plugin.Initializable; -import rs09.plugin.ClassScanner; - -/** - * Handles a gravestone npc. - * @author Vexia - */ -@Initializable -public class GraveStoneNPC extends AbstractNPC { - - /** - * The owner of the gravestone. - */ - private String owner; - - /** - * The display name. - */ - private String display; - - /** - * The gravestone type. - */ - private GraveType type; - - /** - * The life decay time. - */ - private int life = -1; - - /** - * If the stone is blessed. - */ - private boolean blessed; - - /** - * The ground items list. - */ - private List items; - - /** - * Constructs a new {@Code GraveStoneNPC} {@Code Object} - */ - public GraveStoneNPC() { - super(-1, null); - } - - /** - * Constructs a new {@Code GraveStoneNPC} {@Code Object} - * @param id the id. - * @param location the location. - */ - public GraveStoneNPC(int id, Location location) { - super(id, location); - super.setWalks(false); - super.setNeverWalks(true); - } - - @Override - public void init() { - super.init(); - Player player = Repository.getPlayerByName(owner); - if (player != null) { - HintIconManager.registerHintIcon(player, this); - } - lock(); - animate(Animation.create(7394)); - } - - @Override - public void handleTickActions() { - if (life == -1) { - return; - } - if (life < GameWorld.getTicks()) { - clear(); - message("Your gravestone has collapsed."); - return; - } - int minutes = getMinutes(); - if (minutes <= 1) { - int seconds = getSeconds(); - int transformId = -1; - if (seconds == 100) { - transformId = getOriginalId() + 1; - } else if (seconds == 30) { - transformId = getOriginalId() + 2; - } - if (transformId != -1) { - transform(transformId); - } - } - } - - @Override - public void clear() { - super.clear(); - Player player = Repository.getPlayerByName(owner); - if (player != null) { - player.getHintIconManager().clear(); - } - if (owner != null) { - GraveManager.getGraves().remove(owner); - } - } - - @Override - public boolean isAttackable(Entity entity, CombatStyle style, boolean message) { - return false; - } - - @Override - public boolean canAttack(final Entity victim) { - return false; - } - - @Override - public Object fireEvent(String identifier, Object... objects) { - switch (identifier) { - case "updateItems": - for (GroundItem item : items) { - if (item == null) { - continue; - } - item.setDropper((Player) objects[0]); - } - break; - } - return null; - } - - @SuppressWarnings("unchecked") - @Override - public AbstractNPC construct(int id, Location location, Object... objects) { - GraveStoneNPC npc = new GraveStoneNPC(id, location); - if (objects != null && objects.length > 1) { - npc.setOwner((String) objects[0]); - npc.setLife((int) objects[1]); - npc.setItems((List) objects[2]); - npc.setType((GraveType) objects[3]); - npc.setDisplay((String) objects[4]); - } - GraveManager.getGraves().put(npc.getOwner(), npc); - return npc; - } - - @Override - public Plugin newInstance(Object arg) throws Throwable { - ClassScanner.definePlugin(new GraveStonePlugin()); - return super.newInstance(arg); - } - - @Override - public int[] getIds() { - return new int[] { 6565, 6568, 6571, 6574, 6577, 6580, 6583, 6586, 6589, 6592, 6595, 6598, 6601 }; - } - - /** - * Reads the grave message. - * @param player the player. - */ - public void read(Player player) { - player.getConfigManager().set(1146, 1); - player.getInterfaceManager().open(new Component(266)); - if (!isOwner(player.getName())) { - player.getPacketDispatch().sendString(getMessage(), 266, 23); - } else { - final int minutes = getMinutes(); - String message; - if (minutes < 1) { - int seconds = getSeconds(); - message = "It looks like it'll survive another " + (seconds > 1 ? seconds + " seconds." : "second."); - } else { - message = "It looks like it'll survive another " + (minutes > 1 ? minutes + " minutes" : "minute") + "."; - } - player.getPacketDispatch().sendMessage(message); - player.getPacketDispatch().sendString(getMessage(), 266, 23); - } - } - - /** - * Repairs the grave. - * @param player the player. - */ - public void repair(Player player) { - if (player.getSkills().getStaticLevel(Skills.PRAYER) < 2) { - player.getDialogueInterpreter().sendDialogue("You need a prayer level of 2 to repair a gravestone."); - return; - } - if (getId() == getOriginalId()) { - player.sendMessage("This grave does not need repairing."); - return; - } - int seconds = type.getDecay() * 60; - int ticks = (1000 * seconds) / 600; - reTransform(); - updateItems(ticks); - setLife(GameWorld.getTicks() + ticks); - } - - /** - * Blesses the grave. - * @param player the player. - */ - public void bless(Player player) { - if (isOwner(player.getName())) { - player.getPacketDispatch().sendMessage("The gods don't seem to approve of people attempting to bless their own"); - player.getPacketDispatch().sendMessage("gravestones."); - return; - } - if (player.getSkills().getStaticLevel(Skills.PRAYER) < 70) { - player.getDialogueInterpreter().sendDialogue("You need a prayer level of 70 to bless a gravestone."); - return; - } - if (player.getSkills().getPrayerPoints() == 0) { - player.getDialogueInterpreter().sendDialogue("You don't have enough prayer points to do that."); - return; - } - if (isBlessed()) { - player.getPacketDispatch().sendMessage("This gravestone has already been blessed."); - return; - } - reTransform(); - setBlessed(true); - updateItems(6100); - player.animate(Animation.create(645)); - graphics(Graphics.create(1274)); - setLife(GameWorld.getTicks() + 6000); - message(player.getUsername() + " has blessed your grave, it will remain for another 60 minutes."); - } - - /** - * Demolishes the grave. - * @param player the player. - */ - public void demolish(Player player) { - if (!isOwner(player.getName())) { - player.getPacketDispatch().sendMessage("This is not your grave!"); - return; - } - clear(); - player.sendMessage("You destroyed your grave!"); - } - - /** - * Updates the items with a new decay. - * @param ticks the ticks. - */ - private void updateItems(int ticks) { - if (getItems() != null) { - for (GroundItem item : getItems()) { - if (item == null) { - continue; - } - if (item.isActive()) { - item.setDecayTime(ticks); - } - } - } - } - - /** - * Messages the owner. - * @param message the message. - */ - private void message(String message) { - Player o = Repository.getPlayerByName(owner); - if (o != null && o.isActive()) { - o.sendMessage(message); - } - } - - /** - * Gets the minutes left. - * @return the minutes. - */ - public int getMinutes() { - return (life - GameWorld.getTicks()) / 100; - } - - /** - * Gets the seconds left. - * @return the seconds. - */ - public int getSeconds() { - return (life - GameWorld.getTicks()) * 600 / 1000; - } - - /** - * Gets the message. - * @return the message. - */ - public String getMessage() { - int mins = getMinutes(); - return type.getMessage().replace("@name", display).replace("@mins", (mins > 1 ? mins + " minutes" : mins + " minute")); - } - - /** - * Checks if this name is the owner. - * @param name the name. - * @return {@code True} if so. - */ - public boolean isOwner(String name) { - return owner.equals(name); - } - - /** - * Gets the owner. - * @return the owner - */ - public String getOwner() { - return owner; - } - - /** - * Gets the display. - * @return the display - */ - public String getDisplay() { - return display; - } - - /** - * Sets the badisplay. - * @param display the display to set. - */ - public void setDisplay(String display) { - this.display = display; - } - - /** - * Sets the baowner. - * @param owner the owner to set. - */ - public void setOwner(String owner) { - this.owner = owner; - } - - /** - * Gets the life. - * @return the life - */ - public int getLife() { - return life; - } - - /** - * Sets the balife. - * @param life the life to set. - */ - public void setLife(int life) { - this.life = life; - } - - /** - * Gets the type. - * @return the type - */ - public GraveType getType() { - return type; - } - - /** - * Sets the batype. - * @param type the type to set. - */ - public void setType(GraveType type) { - this.type = type; - } - - /** - * Gets the blessed. - * @return the blessed - */ - public boolean isBlessed() { - return blessed; - } - - /** - * Sets the bablessed. - * @param blessed the blessed to set. - */ - public void setBlessed(boolean blessed) { - this.blessed = blessed; - } - - /** - * Gets the items. - * @return the items - */ - public List getItems() { - return items; - } - - /** - * Sets the baitems. - * @param items the items to set. - */ - public void setItems(List items) { - this.items = items; - } - - /** - * Handles the interactions of a grave stone. - * @author Vexia - */ - public class GraveStonePlugin extends OptionHandler { - - @Override - public Plugin newInstance(Object arg) throws Throwable { - for (GraveType g : GraveType.values()) { - for (int start = g.getNpcId(); start < g.getNpcId() + 3; start++) { - NPCDefinition.forId(start).getHandlers().put("option:read", this); - NPCDefinition.forId(start).getHandlers().put("option:bless", this); - NPCDefinition.forId(start).getHandlers().put("option:demolish", this); - NPCDefinition.forId(start).getHandlers().put("option:repair", this); - } - } - return this; - } - - @Override - public boolean handle(Player player, Node node, String option) { - if (!(node instanceof GraveStoneNPC)) { - player.sendMessage("Error! NPC is not instanceof a GraveStoneNPC."); - return true; - } - GraveStoneNPC grave = (GraveStoneNPC) node; - switch (option) { - case "read": - grave.read(player); - break; - case "bless": - grave.bless(player); - break; - case "demolish": - grave.demolish(player); - break; - case "repair": - grave.repair(player); - break; - } - return true; - } - - } -} diff --git a/Server/src/main/java/core/game/node/entity/player/Player.java b/Server/src/main/java/core/game/node/entity/player/Player.java index 88d88d352..ff4ce7be1 100644 --- a/Server/src/main/java/core/game/node/entity/player/Player.java +++ b/Server/src/main/java/core/game/node/entity/player/Player.java @@ -25,7 +25,6 @@ import core.game.node.entity.player.link.appearance.Appearance; import core.game.node.entity.player.link.audio.AudioManager; import core.game.node.entity.player.link.diary.AchievementDiaryManager; import core.game.node.entity.player.link.emote.EmoteManager; -import core.game.node.entity.player.link.grave.GraveManager; import core.game.node.entity.player.link.music.MusicPlayer; import core.game.node.entity.player.link.prayer.Prayer; import core.game.node.entity.player.link.prayer.PrayerType; @@ -35,7 +34,6 @@ import core.game.node.entity.player.link.skillertasks.SkillerTasks; import core.game.node.entity.skill.Skills; import core.game.node.entity.skill.construction.HouseManager; import core.game.node.entity.skill.summoning.familiar.FamiliarManager; -import core.game.node.item.GroundItem; import core.game.node.item.GroundItemManager; import core.game.node.item.Item; import core.game.system.communication.CommunicationInfo; @@ -65,13 +63,15 @@ import kotlin.Unit; import kotlin.jvm.functions.Function1; import org.rs09.consts.Items; import proto.management.ClanLeaveNotification; -import proto.management.LeaveClanRequest; import proto.management.PlayerStatusUpdate; import rs09.GlobalStats; import rs09.ServerConstants; import rs09.game.VarpManager; import rs09.game.node.entity.combat.CombatSwingHandler; import rs09.game.node.entity.combat.equipment.EquipmentDegrader; +import rs09.game.node.entity.player.graves.Grave; +import rs09.game.node.entity.player.graves.GraveType; +import rs09.game.node.entity.player.graves.GraveController; import rs09.game.node.entity.player.info.login.PlayerSaver; import rs09.game.node.entity.skill.runecrafting.PouchManager; import rs09.game.node.entity.state.newsys.State; @@ -230,11 +230,6 @@ public class Player extends Entity { */ private final SkullManager skullManager = new SkullManager(this); - /** - * The grave stone manager. - */ - private final GraveManager graveManager = new GraveManager(this); - /** * The familiar manager. */ @@ -608,35 +603,28 @@ public class Player extends Entity { } GroundItemManager.create(new Item(526), getLocation(), k); final Container[] c = DeathTask.getContainers(this); - boolean gravestone = graveManager.generateable() && getIronmanManager().getMode() != IronmanMode.ULTIMATE; - int seconds = graveManager.getType().getDecay() * 60; - int ticks = (1000 * seconds) / 600; - List items = new ArrayList<>(20); - for (Item item : c[1].toArray()) { - if (item != null) { - GroundItem ground; - if (item.hasItemPlugin()) { - item = item.getPlugin().getDeathItem(item); - } - if (gravestone || !item.getDefinition().isTradeable()) { - ground = new GroundItem(item, getLocation(), gravestone ? ticks + 100 : 200, this); - } else { - ground = new GroundItem(item.getDropItem(), getLocation(), k); - } - items.add(ground); - ground.setDropper(this); //Checking for ironman mode in any circumstance for death items is inaccurate to how it works in both 2009scapes. - GroundItemManager.create(ground); + + boolean canCreateGrave = GraveController.allowGenerate(this); + if (canCreateGrave) { + Grave g = GraveController.produceGrave(GraveController.getGraveType(this)); + g.initialize(this, location, Arrays.stream(c[1].toArray()).filter(Objects::nonNull).toArray(Item[]::new)); //note: the amount of code required to filter nulls from an array in Java is atrocious. + } else { + for (Item item : c[1].toArray()) { + if (item == null) continue; + if (GraveController.shouldCrumble(item.getId())) + continue; + if (GraveController.shouldRelease(item.getId())) + continue; + item = GraveController.checkTransform(item); + GroundItemManager.create(item, location, killer instanceof Player ? (Player) killer : this); } + sendMessage(colorize("%RDue to the circumstances of your death, you do not have a grave.")); } + equipment.clear(); inventory.clear(); inventory.addAll(c[0]); - if (gravestone) { - graveManager.create(ticks, items); - sendMessages("Because of your current gravestone, you have "+graveManager.getType().getDecay()+" minutes to get your items and", "equipment back after dying in combat."); - } familiarManager.dismiss(); - } skullManager.setSkulled(false); removeAttribute("combat-time"); @@ -1215,14 +1203,6 @@ public class Player extends Entity { return houseManager; } - /** - * Gets the graveManager. - * @return the graveManager - */ - public GraveManager getGraveManager() { - return graveManager; - } - /** * Gets the audioManager. * @return the audioManager diff --git a/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java b/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java index c142b40c4..d5a134252 100644 --- a/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java +++ b/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java @@ -223,7 +223,6 @@ public final class LoginConfiguration { player.getPacketDispatch().sendString("Friends List - World " + GameWorld.getSettings().getWorldId(), 550, 3); player.getConfigManager().init(); player.getQuestRepository().syncronizeTab(player); - player.getGraveManager().update(); player.getInterfaceManager().close(); player.getEmoteManager().refresh(); player.getInterfaceManager().openInfoBars(); diff --git a/Server/src/main/java/core/game/node/entity/player/link/grave/GraveManager.java b/Server/src/main/java/core/game/node/entity/player/link/grave/GraveManager.java deleted file mode 100644 index db3ea7c59..000000000 --- a/Server/src/main/java/core/game/node/entity/player/link/grave/GraveManager.java +++ /dev/null @@ -1,151 +0,0 @@ -package core.game.node.entity.player.link.grave; - -import core.game.node.entity.npc.AbstractNPC; -import core.game.node.entity.npc.NPC; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.info.Rights; - -import core.game.node.entity.player.link.HintIconManager; -import core.game.node.entity.player.link.prayer.PrayerType; -import core.game.node.item.GroundItem; -import rs09.game.world.GameWorld; - -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Manages the players grave stone. - * @author Vexia - */ -public class GraveManager { - - /** - * The current grave stones in the world. - */ - private static final Map graves = new HashMap<>(); - - /** - * The player instance. - */ - private final Player player; - - /** - * The grave type. - */ - private GraveType type = GraveType.MEMORIAL_PLAQUE; - - /** - * The current gravestone. - */ - private NPC grave; - - /** - * Constructs a new {@Code GraveManager} {@Code Object} - * @param player the player. - */ - public GraveManager(Player player) { - this.player = player; - } - - /** - * Creates a grave. - * @param ticks the ticks. - * @param items the items. - */ - public void create(int ticks, List items) { - if (hasGrave()) { - grave.clear(); - player.sendMessage("Your previous gravestone has collapsed."); - } - NPC npc = NPC.create(type.getNpcId(), player.getLocation(), player.getName(), GameWorld.getTicks() + ticks, items, type, player.getUsername()); - npc.init(); - setGrave(npc); - } - - /** - * Updates the players grave items. - */ - public void update() { - NPC npc = graves.get(player.getName()); - if (npc != null && npc.isActive()) { - AbstractNPC n = (AbstractNPC) npc; - n.fireEvent("updateItems", player); - HintIconManager.registerHintIcon(player, n); - } - } - - /** - * Checks if a gravestone is generateable at this time. - * @return {@code True} if so. - */ - public boolean generateable() { - if (player.getDetails().getRights() == Rights.ADMINISTRATOR && GameWorld.getSettings().isHosted()) { - return false; - } - if (player.getSkullManager().isSkulled()) { - return false; - } - if (player.getInventory().itemCount() + player.getEquipment().itemCount() <= (player.getPrayer().get(PrayerType.PROTECT_ITEMS) ? 4 : 3)) { - return false; - } - return true; - } - - /** - * Checks if the player has an active grave. - * @return {@code True} if so. - */ - public boolean hasGrave() { - return grave != null && grave.isActive(); - } - - /** - * Gets the type. - * @return the type - */ - public GraveType getType() { - return type; - } - - /** - * Sets the grave type. - * @param type the type to set. - */ - public void setType(GraveType type) { - this.type = type; - } - - /** - * Gets the player. - * @return the player - */ - public Player getPlayer() { - return player; - } - - /** - * Gets the grave. - * @return the grave - */ - public NPC getGrave() { - return grave; - } - - /** - * Sets the bagrave. - * @param grave the grave to set. - */ - public void setGrave(NPC grave) { - this.grave = grave; - } - - /** - * Gets the graves. - * @return the graves - */ - public static Map getGraves() { - return graves; - } -} diff --git a/Server/src/main/java/core/game/node/entity/player/link/grave/GraveType.java b/Server/src/main/java/core/game/node/entity/player/link/grave/GraveType.java deleted file mode 100644 index 721c1c48c..000000000 --- a/Server/src/main/java/core/game/node/entity/player/link/grave/GraveType.java +++ /dev/null @@ -1,74 +0,0 @@ -package core.game.node.entity.player.link.grave; - -/** - * A grave stone type. - * @author Vexia - */ -public enum GraveType { - MEMORIAL_PLAQUE(0, 3, 6565, "In memory of @name, who died here."), FLAG(50, 3, 6568, "In memory of @name, who died here."), SMALL(500, 3, 6571, "In loving memory of our dear friend @name, who died in this place @mins ago."), ORNATE(5000, 4, 6574, "In loving memory of our dear friend @name, who died in this place @mins ago."), FRONT_OF_LIFE(50000, 5, 6577, "In your travels, pause awhile to remember @name, who passed away in this spot."), STELE(50000, 5, 6580, "In your travels, pause awhile to remember @name, who passed away in this spot."), SARADOMIN(50000, 5, 6583, "@name, an enlightened severant of Saradomin, perished in this place."), ZAMRAOK(50000, 5, 6586, "@name a most bloodthirsty follower of Zamorak, perished in this place."), GUTHIX(50000, 5, 6589, "@name walked with the Balance of Guthix, perished in this place."), BANDOS(50000, 5, 6592, "@name, a vicious warrior dedicated to Bandos, perished in this place."), ARMADYL(50000, 5, 6595, "@name a follower of the Law of Aramdyl, perished in this place."), MEMORIAL_STONE(50000, 5, 6598, "@name, servant of the Unknown Power, perished in this place."), ANGEL_OF_DEATH(500000, 6, 6601, "Ye frails who gaze upon this sight, forget not the date of @name, once mighty, now surrendered to the inescapable grasp of destiny, Requiescat in pace."); - - /** - * The cost of the grave stone. - */ - private final int cost; - - /** - * The decay time of this gravestone. - */ - private final int decay; - - /** - * The npc id of this gravestone. - */ - private final int npcId; - - /** - * The message to display on the grave. - */ - private final String message; - - /** - * Constructs a new {@code GraveType} {@code Object}. - * @param cost the cost. - * @param decay the decay. - */ - GraveType(final int cost, final int decay, final int npc, final String message) { - this.cost = cost; - this.decay = decay; - this.npcId = npc; - this.message = message; - } - - /** - * Gets the decay. - * @return the decay - */ - public int getDecay() { - return decay; - } - - /** - * Gets the cost. - * @return the cost - */ - public int getCost() { - return cost; - } - - /** - * Gets the npcId. - * @return the npcId - */ - public int getNpcId() { - return npcId; - } - - /** - * Gets the message. - * @return the message - */ - public String getMessage() { - return message; - } - -} diff --git a/Server/src/main/java/core/game/node/item/GroundItem.java b/Server/src/main/java/core/game/node/item/GroundItem.java index c67501267..5b060feff 100644 --- a/Server/src/main/java/core/game/node/item/GroundItem.java +++ b/Server/src/main/java/core/game/node/item/GroundItem.java @@ -15,6 +15,8 @@ public class GroundItem extends Item { */ private Player dropper; + private int dropperUid; + /** * The amount of ticks. */ @@ -62,6 +64,12 @@ public class GroundItem extends Item { this(item, location, 200, player); } + public GroundItem(Item item, Location location, int playerUid, int ticks) { + this(item, location); + this.dropperUid = playerUid; + this.decayTime = ticks; + } + /** * Constructs a new {@code GroundItem} {@code Object}. * @param item The item. @@ -74,6 +82,7 @@ public class GroundItem extends Item { super.index = -1; super.interaction.setDefault(); this.dropper = player; + this.dropperUid = player != null ? player.getDetails().getUid() : -1; this.ticks = GameWorld.getTicks(); this.decayTime = ticks + decay; } @@ -104,7 +113,7 @@ public class GroundItem extends Item { * @return {@code True} if so. */ public boolean droppedBy(Player p) { - if (dropper != null && p.getDetails().getUid() == dropper.getDetails().getUid()) { + if (p.getDetails().getUid() == dropperUid) { dropper = p; return true; } @@ -191,6 +200,9 @@ public class GroundItem extends Item { this.removed = removed; } + public int getDropperUid() { + return dropperUid; + } @Override public String toString() { return "GroundItem [dropper=" + (dropper != null ? dropper.getUsername() : dropper) + ", ticks=" + ticks + ", decayTime=" + decayTime + ", remainPrivate=" + remainPrivate + ", removed=" + removed + "]"; diff --git a/Server/src/main/java/core/game/node/item/GroundItemManager.java b/Server/src/main/java/core/game/node/item/GroundItemManager.java index fc5d797ad..1d722e666 100644 --- a/Server/src/main/java/core/game/node/item/GroundItemManager.java +++ b/Server/src/main/java/core/game/node/item/GroundItemManager.java @@ -32,6 +32,10 @@ public final class GroundItemManager { return create(new GroundItem(item, location, null)); } + public static GroundItem create (Item item, Location location, int playerUid, int ticks) { + return create(new GroundItem(item, location, playerUid, ticks)); + } + /** * Creates a ground item. * @param item the item. diff --git a/Server/src/main/java/core/game/system/SystemTermination.java b/Server/src/main/java/core/game/system/SystemTermination.java index f0e2a3abc..96c37ac17 100644 --- a/Server/src/main/java/core/game/system/SystemTermination.java +++ b/Server/src/main/java/core/game/system/SystemTermination.java @@ -5,6 +5,7 @@ import api.ShutdownListener; import core.game.node.entity.player.Player; import rs09.Server; import rs09.ServerConstants; +import rs09.ServerStore; import rs09.game.ai.AIRepository; import rs09.game.system.SystemLogger; import rs09.game.world.GameWorld; @@ -54,7 +55,16 @@ public final class SystemTermination { } } GameWorld.getShutdownListeners().forEach(ShutdownListener::shutdown); - GameWorld.getWorldPersists().forEach(PersistWorld::save); + ServerStore s = null; + for (PersistWorld wld : GameWorld.getWorldPersists()) { + if (wld instanceof ServerStore) + s = (ServerStore) wld; + else + wld.save(); + } + //ServerStore should ***always*** save last. Fudging a race condition here :) + if (s != null) + s.save(); if(ServerConstants.DATA_PATH != null) save(ServerConstants.DATA_PATH); } catch (Throwable e) { diff --git a/Server/src/main/java/core/game/world/map/Location.java b/Server/src/main/java/core/game/world/map/Location.java index 7e6a32e00..325563d44 100644 --- a/Server/src/main/java/core/game/world/map/Location.java +++ b/Server/src/main/java/core/game/world/map/Location.java @@ -442,6 +442,16 @@ public final class Location extends Node { return "[" + x + ", " + y + ", " + z + "]"; } + public static Location fromString(String locString) { + String trimmed = locString.replace("[", "").replace("]", ""); + String[] tokens = trimmed.split(","); + return Location.create( + Integer.parseInt(tokens[0].trim()), + Integer.parseInt(tokens[1].trim()), + Integer.parseInt(tokens[2].trim()) + ); + } + @Override public int hashCode() { return z << 30 | x << 15 | y; diff --git a/Server/src/main/kotlin/api/ContentAPI.kt b/Server/src/main/kotlin/api/ContentAPI.kt index 5928bdce1..e68262ffc 100644 --- a/Server/src/main/kotlin/api/ContentAPI.kt +++ b/Server/src/main/kotlin/api/ContentAPI.kt @@ -2078,6 +2078,8 @@ fun registerHintIcon(player: Player, location: Location, height: Int) { * @param node the Node to register a hint icon for. */ fun registerHintIcon(player: Player, node: Node) { + if (getAttribute(player, "hinticon", null) != null) + return setAttribute(player, "hinticon", HintIconManager.registerHintIcon(player, node)) } diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/graves/Grave.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/Grave.kt new file mode 100644 index 000000000..f0a3505b1 --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/Grave.kt @@ -0,0 +1,175 @@ +package rs09.game.node.entity.player.graves + +import api.TickListener +import api.clearHintIcon +import api.registerHintIcon +import api.sendMessage +import core.game.node.entity.npc.AbstractNPC +import core.game.node.entity.player.Player +import core.game.node.item.GroundItem +import core.game.node.item.GroundItemManager +import core.game.node.item.Item +import core.game.world.map.Location +import core.plugin.Initializable +import org.rs09.consts.NPCs +import rs09.game.world.GameWorld +import rs09.game.world.repository.Repository +import rs09.tools.secondsToTicks +import rs09.tools.stringtools.colorize +import rs09.tools.ticksToSeconds +import kotlin.properties.Delegates + +@Initializable +class Grave : AbstractNPC { + lateinit var type: GraveType + private val items = ArrayList() + var ownerUsername: String = "" + var ownerUid: Int = -1 + + var ticksRemaining = -1 + + constructor() : super(NPCs.GRAVESTONE_6571, Location.create(0,0,0), false) + private constructor(id: Int, location: Location) : super(id, location) + + override fun construct(id: Int, location: Location, vararg objects: Any): AbstractNPC { + return Grave(id, location) + } + + override fun getIds(): IntArray { + return GraveType.ids + } + + fun configureType(type: GraveType) { + this.type = type + this.transform(type.npcId) + this.ticksRemaining = secondsToTicks(type.durationMinutes * 60) + } + + fun initialize(player: Player, location: Location, inventory: Array) { + if (!GraveController.allowGenerate(player)) + return + + this.ownerUid = player.details.uid + this.ownerUsername = player.username + this.location = location + this.isRespawn = false + this.isWalks = false + this.isNeverWalks = true + + for (item in inventory) { + if (GraveController.shouldRelease(item.id)) { + sendMessage(player, "Your ${item.name.lowercase().replace("jar", "")} has escaped.") + continue + } + + if (GraveController.shouldCrumble(item.id)) { + sendMessage(player, "Your ${item.name.lowercase()} has crumbled to dust.") + continue + } + + val finalItem = GraveController.checkTransform(item) + + val gi = GroundItemManager.create(finalItem, this.location, player) + gi.isRemainPrivate = true + gi.decayTime = secondsToTicks(type.durationMinutes * 60) + this.items.add(gi) + } + + if (items.isEmpty()) { + clear() + return + } + + this.init() + + if (GraveController.activeGraves[ownerUid] != null) { + val oldGrave = GraveController.activeGraves[ownerUid] + oldGrave?.collapse() + } + + GraveController.activeGraves[ownerUid] = this + sendMessage(player, colorize("%RBecause of your current gravestone, you have ${type.durationMinutes} minutes to get your items back.")) + } + + fun setupFromJsonParams(playerUid: Int, ticks: Int, location: Location, items: Array, username: String) { + this.ownerUid = playerUid + this.ticksRemaining = ticks + this.location = location + this.isRespawn = false + this.isWalks = false + this.isNeverWalks = true + this.ownerUsername = username + + for (item in items) { + val gi = GroundItemManager.create(item, location, playerUid, GameWorld.ticks + ticksRemaining) + gi.isRemainPrivate = true + this.items.add(gi) + } + + this.transform(type.npcId) + this.init() + } + + override fun tick() { + //Grave should not do anything else on tick, that is all handled by GraveController. + if (Repository.uid_map[ownerUid] != null) { + val p = Repository.uid_map[ownerUid] ?: return + registerHintIcon(p, this) + } + } + + fun addTime(ticks: Int) { + ticksRemaining += ticks + for (gi in items) { + gi.decayTime = ticksRemaining + } + if (ticksRemaining < 30) + transform(type.npcId + 2) + else if (ticksRemaining < 90) + transform(type.npcId + 1) + else + transform(type.npcId) + } + + fun collapse() { + for (item in items) { + GroundItemManager.destroy(item) + } + clear() + GraveController.activeGraves.remove(ownerUid) + if (Repository.uid_map[ownerUid] != null) { + val p = Repository.uid_map[ownerUid] ?: return + clearHintIcon(p) + } + } + + fun demolish() { + val owner = Repository.uid_map[ownerUid] ?: return + for (item in items) { + if (!item.isRemoved) + item.decayTime = secondsToTicks(45) + } + clear() + sendMessage(owner, "It looks like it'll last another ${getFormattedTimeRemaining()}.") + sendMessage(owner, "You demolish it anyway.") + GraveController.activeGraves.remove(ownerUid) + clearHintIcon(owner) + } + + fun getItems() : Array { + return this.items.toTypedArray() + } + + fun retrieveFormattedText(): String { + return type.text + .replace("@name", ownerUsername) + .replace("@mins", getFormattedTimeRemaining()) + } + + fun getFormattedTimeRemaining() : String { + val seconds = ticksToSeconds(ticksRemaining) + val timeQty = if (seconds / 60 > 0) seconds / 60 else seconds + val timeUnit = (if (seconds / 60 > 0) "minute" else "second") + if (timeQty > 1) "s" else "" + return "$timeQty $timeUnit" + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GraveController.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GraveController.kt new file mode 100644 index 000000000..153acd652 --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GraveController.kt @@ -0,0 +1,286 @@ +package rs09.game.node.entity.player.graves + +import api.* +import core.game.interaction.Interaction +import core.game.node.Node +import core.game.node.entity.combat.ImpactHandler +import core.game.node.entity.player.Player +import core.game.node.entity.player.info.Rights +import core.game.node.entity.player.link.IronmanMode +import core.game.node.entity.player.link.audio.Audio +import core.game.node.entity.skill.Skills +import core.game.node.item.Item +import core.game.system.task.Pulse +import core.game.world.map.Location +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import org.rs09.consts.Items +import rs09.ServerStore +import rs09.game.interaction.InteractionListener +import rs09.game.system.command.Privilege +import rs09.game.world.GameWorld +import rs09.game.world.repository.Repository +import rs09.tools.secondsToTicks +import rs09.tools.stringtools.colorize +import rs09.tools.ticksToSeconds +import java.util.Map +import kotlin.math.min + +class GraveController : PersistWorld, TickListener, InteractionListener, Commands { + override fun defineListeners() { + on(GraveType.ids, NPC, "read", handler = this::onGraveReadOption) + on(GraveType.ids, NPC, "bless", handler = this::onGraveBlessed) + on(GraveType.ids, NPC, "repair", handler = this::onGraveRepaired) + on(GraveType.ids, NPC, "demolish", handler = this::onGraveDemolished) + } + + override fun defineCommands() { + define("forcegravedeath", Privilege.ADMIN, "", "Forces a death that should produce a grave.") {player, _ -> + 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}") + GameWorld.Pulser.submit(object : Pulse(15) { + override fun pulse(): Boolean { + player.details.rights = Rights.ADMINISTRATOR + sendMessage(player, "Rights restored") + return true + } + }) + } + } + + + override fun tick() { + for (grave in activeGraves.values.toTypedArray()) { + if (grave.ticksRemaining == -1) return + + if (grave.ticksRemaining == secondsToTicks(30) || grave.ticksRemaining == secondsToTicks(90)) { + grave.transform(grave.id + 1) + } + + if (grave.ticksRemaining == 0) { + grave.collapse() + } + + grave.ticksRemaining-- + } + } + + private fun onGraveReadOption(player: Player, node: Node) : Boolean { + val grave = node as? Grave ?: return false + + var isGraniteBackground = false + + when (grave.type) { + in GraveType.SMALL_GS..GraveType.ANGEL_DEATH -> isGraniteBackground = true + } + + if (isGraniteBackground) + setVarbit(player, 4191, 1) + else + setVarbit(player, 4191, 0) + + openInterface(player, 266) + setInterfaceText(player, grave.retrieveFormattedText(), 266, 23) + + if (player.details.uid == grave.ownerUid) { + sendMessage(player, "It looks like it'll survive another ${grave.getFormattedTimeRemaining()}.") + sendMessage(player, "Isn't there something a bit odd about reading your own gravestone?") + } + return true + } + + private fun onGraveBlessed(player: Player, node: Node) : Boolean { + val g = node as? Grave ?: return false + + if (getAttribute(g, "blessed", false)) { + sendMessage(player, "This grave has already been blessed.") + return true + } + + if (player.details.uid == g.ownerUid) { + sendMessage(player, "The gods don't seem to approve of people attempting to bless their own gravestones.") + return true + } + + if (getStatLevel(player, Skills.PRAYER) < 70) { + sendMessage(player, "You need a Prayer level of 70 to bless a grave.") + return true + } + + val blessAmount = min(60, player.skills.prayerPoints.toInt() - 10) + + if (blessAmount <= 0) { + sendMessage(player, "You do not have enough prayer points to do that.") + return true + } + + g.addTime(secondsToTicks(blessAmount * 60)) + player.skills.prayerPoints -= blessAmount + setAttribute(g, "blessed", true) + + playAudio(player, Audio(2674)) + animate(player, 645) + + val gOwner = Repository.uid_map[g.ownerUid] + if (gOwner != null) { + sendMessage(gOwner, colorize("%RYour grave has been blessed.")) + } + return true + } + + private fun onGraveRepaired(player: Player, node: Node) : Boolean { + val g = node as? Grave ?: return false + + if (getAttribute(g, "repaired", false)) { + sendMessage(player, "This grave has already been repaired.") + return true + } + + if (getStatLevel(player, Skills.PRAYER) < 2) { + sendMessage(player, "You need a Prayer level of 2 to bless a grave.") + return true + } + + if (player.skills.prayerPoints < 1.0) { + sendMessage(player, "You do not have enough prayer points to do that.") + return true + } + + val restoreAmount = min(5, player.skills.prayerPoints.toInt()) + g.addTime(secondsToTicks(restoreAmount * 60)) + player.skills.prayerPoints -= restoreAmount + setAttribute(g, "repaired", true) + + playAudio(player, Audio(2674)) + animate(player, 645) + return true + } + + private fun onGraveDemolished(player: Player, node: Node) : Boolean { + val g = node as? Grave ?: return false + + if (player.details.uid != g.ownerUid) { + sendMessage(player, "You cannot demolish someone else's gravestone!") + return true + } + + g.demolish() + return true + } + + override fun save() { + serializeToServerStore() + } + + override fun parse() { + deserializeFromServerStore() + } + + companion object { + val activeGraves = HashMap() + var childCounter = 0 + val ATTR_GTYPE = "/save:gravetype" + + @JvmStatic fun produceGrave(type: GraveType): Grave { + val g = Grave() + g.configureType(type) + return g + } + + @JvmStatic fun shouldCrumble(item: Int) : Boolean { + when (item) { + Items.ECTOPHIAL_4251 -> return true + in Items.SMALL_POUCH_5509..Items.GIANT_POUCH_5515 -> return true + } + + return itemDefinition(item).hasAction("destroy") + } + + @JvmStatic fun shouldRelease(item: Int) : Boolean { + when (item) { + Items.CHINCHOMPA_9976 -> return true + Items.CHINCHOMPA_10033 -> return true + in Items.BABY_IMPLING_JAR_11238..Items.DRAGON_IMPLING_JAR_11257 -> return itemDefinition(item).isUnnoted + } + + return false + } + + @JvmStatic fun checkTransform(item: Item) : Item { + if (item.hasItemPlugin()) + return item.plugin.getDeathItem(item) + return item + } + + @JvmStatic fun allowGenerate(player: Player) : Boolean { + if (player.skullManager.isSkulled) + return false + if (player.skullManager.isWilderness) + return false + if (player.ironmanManager.mode == IronmanMode.HARDCORE) + return false + return true + } + + @JvmStatic fun getGraveType(player: Player) : GraveType { + return GraveType.values()[getAttribute(player, ATTR_GTYPE, 0)] + } + + @JvmStatic fun updateGraveType(player: Player, type: GraveType) { + setAttribute(player, ATTR_GTYPE, type.ordinal) + } + + @JvmStatic fun hasGraveAt(loc: Location) : Boolean { + return activeGraves.values.toTypedArray().any { it.location == loc } + } + + fun serializeToServerStore() { + val archive = ServerStore.getArchive("active-graves") + for ((uid,grave) in activeGraves) { + val g = JSONObject() + g["ticksRemaining"] = grave.ticksRemaining + g["location"] = grave.location.toString() + g["type"] = grave.type.ordinal + g["username"] = grave.ownerUsername + val items = JSONArray() + for (item in grave.getItems()) { + val i = JSONObject() + i["id"] = item.id + i["amount"] = item.amount + i["charge"] = item.charge + items.add(i) + } + g["items"] = items + archive["$uid"] = g + } + } + + fun deserializeFromServerStore() { + val archive = ServerStore.getArchive("active-graves") + for (entry in archive.entries as Set>) { + val g = entry.value as JSONObject + val uid = (entry.key as String).toInt() + val type = g["type"].toString().toInt() + val ticks = g["ticksRemaining"].toString().toInt() + val location = Location.fromString(g["location"].toString()) + val username = g["username"].toString() + + val items = ArrayList() + val itemsRaw = g["items"] as JSONArray + for (itemRaw in itemsRaw) { + val item = itemRaw as JSONObject + val id = item["id"].toString().toInt() + val amount = item["amount"].toString().toInt() + val charge = item["charge"].toString().toInt() + items.add(Item(id, amount, charge)) + } + + val grave = produceGrave(GraveType.values()[type]) + grave.setupFromJsonParams(uid, ticks, location, items.toTypedArray(), username) + activeGraves[uid] = grave + } + } + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GravePurchaseInterface.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GravePurchaseInterface.kt new file mode 100644 index 000000000..1a5598abf --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GravePurchaseInterface.kt @@ -0,0 +1,65 @@ +package rs09.game.node.entity.player.graves + +import api.* +import core.game.node.item.Item +import org.rs09.consts.Components +import org.rs09.consts.Items +import rs09.game.interaction.InterfaceListener + +class GravePurchaseInterface : InterfaceListener { + val BUTTON_CONFIRM = 34 + val AVAILABLE_GRAVES_BITFIELD = 0xFFF //Enable all graves, authentically the quest-locked ones should be excluded from the bitfield, but y'know. + val AVAILABLE_GRAVES_VARBIT = 4191 + val CURRENT_GRAVE_VARBIT = 4190 + + override fun defineInterfaceListeners() { + onOpen (Components.GRAVESTONE_SHOP_652) {player, _ -> + val userType = GraveController.getGraveType(player).ordinal + setVarbit(player, AVAILABLE_GRAVES_VARBIT, AVAILABLE_GRAVES_BITFIELD) + setVarbit(player, CURRENT_GRAVE_VARBIT, userType) + + val settings = IfaceSettingsBuilder() + .enableAllOptions() + .build() + player.packetDispatch.sendIfaceSettings(settings, 34, Components.GRAVESTONE_SHOP_652, 0, 13) + return@onOpen true + } + + on (Components.GRAVESTONE_SHOP_652, BUTTON_CONFIRM) { player, _, _, _, slot, _ -> + val selectedType = GraveType.values()[slot] + val userType = GraveController.getGraveType(player) + val activeGrave = GraveController.activeGraves[player.details.uid] + + if (activeGrave != null){ + sendDialogue(player, "You cannot change graves while you have a grave active.") + return@on true + } + + if (selectedType == userType) { + sendDialogue(player, "You already have that gravestone!") + return@on true + } + + val cost = selectedType.cost + val requirement = selectedType.requiredQuest + + if (requirement.isNotEmpty() && !isQuestComplete(player, requirement)) { + sendDialogue(player, "That gravestone requires completion of $requirement.") + return@on true + } + + if (selectedType != GraveType.MEM_PLAQUE && amountInInventory(player, Items.COINS_995) < cost) { + sendDialogue(player, "You do not have enough coins to afford that gravestone.") + return@on true + } + + if (selectedType == GraveType.MEM_PLAQUE || removeItem(player, Item(995, cost))) { + GraveController.updateGraveType(player, selectedType) + sendDialogue(player, "Your grave has been updated.") + } + + closeInterface(player) + return@on true + } + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GraveType.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GraveType.kt new file mode 100644 index 000000000..afb664c45 --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/graves/GraveType.kt @@ -0,0 +1,28 @@ +package rs09.game.node.entity.player.graves + +import org.rs09.consts.NPCs + +enum class GraveType(val npcId: Int, val cost: Int, val durationMinutes: Int, val isMembers: Boolean, val requiredQuest: String = "", val text: String) { + MEM_PLAQUE(NPCs.GRAVE_MARKER_6565, 0, 2, false, text = "In memory of @name,
who died here."), + FLAG(NPCs.GRAVE_MARKER_6568, 50, 2, false, text = MEM_PLAQUE.text), + SMALL_GS(NPCs.GRAVESTONE_6571, 500, 2, false, text = "In loving memory of our dear friend @name,
who died in this place @mins ago."), + ORNATE_GS(NPCs.GRAVESTONE_6574, 5000, 3, false, text = SMALL_GS.text), + FONT_OF_LIFE(NPCs.GRAVESTONE_6577, 50000, 4, true, text = "In your travels,
pause awhile to remember @name,
who passed away at this spot."), + STELE(NPCs.STELE_6580, 50000, 4, true, text = FONT_OF_LIFE.text), + SARA_SYMBOL(NPCs.SARADOMIN_SYMBOL_6583, 50000, 4, true, text = "@name,
an enlightened servant of Saradomin,
perished in this place."), + ZAM_SYMBOL(NPCs.ZAMORAK_SYMBOL_6586, 50000, 4, true, text = "@name,
a most bloodthirsty follower of Zamorak,
perished in this place."), + GUTH_SYMBOL(NPCs.GUTHIX_SYMBOL_6589, 50000, 4, true, text = "@name,
who walked with the Balance of Guthix,
perished in this place."), + BAND_SYMBOL(NPCs.BANDOS_SYMBOL_6592, 50000, 4, true, requiredQuest = "Land of the Goblins", text = "@name,
a vicious warrior dedicated to Bandos,
perished in this place. "), + ARMA_SYMBOL(NPCs.ARMADYL_SYMBOL_6595, 50000, 4, true, requiredQuest = "Temple of Ikov", text = "@name,
a follower of the Law of Armadyl,
perished in this place."), + ZARO_SYMBOL(NPCs.MEMORIAL_STONE_6598, 50000, 4, true, requiredQuest = "Desert Treasure", text = "@name,
servant of the Unknown Power,
perished in this place."), + ANGEL_DEATH(NPCs.MEMORIAL_STONE_6601, 500000, 5, true, text = "Ye frail mortals who gaze upon this sight,
forget not the fate of @name, once mighty, now
surrendered to the inescapable grasp of destiny.
Requiescat in pace."); + + companion object { + val ids = values().fold(ArrayList()) {list, type -> + list.add(type.npcId) + list.add(type.npcId + 1) + list.add(type.npcId + 2) + list + }.toIntArray() + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaveParser.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaveParser.kt index 2af90200e..2e677bb3a 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaveParser.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaveParser.kt @@ -1,20 +1,18 @@ package rs09.game.node.entity.player.info.login import api.PersistPlayer -import core.game.interaction.item.brawling_gloves.BrawlingGloves import core.game.node.entity.combat.CombatSpell import core.game.node.entity.player.Player import core.game.node.entity.player.link.IronmanMode import core.game.node.entity.player.link.SpellBookManager import core.game.node.entity.player.link.emote.Emotes -import core.game.node.entity.player.link.grave.GraveType import core.game.node.entity.player.link.music.MusicEntry -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import org.json.simple.JSONArray import org.json.simple.JSONObject import org.json.simple.parser.JSONParser import rs09.ServerConstants +import rs09.game.node.entity.player.graves.GraveController +import rs09.game.node.entity.player.graves.GraveType import rs09.game.node.entity.skill.farming.CompostBins import rs09.game.node.entity.skill.farming.FarmingPatch import rs09.game.system.SystemLogger @@ -287,8 +285,9 @@ class PlayerSaveParser(val player: Player) { fun parseGrave() { saveFile ?: return - val graveData = (saveFile!!["grave_type"] as String).toInt() - player.graveManager.type = GraveType.values()[graveData] + val graveData = (saveFile!!["grave_type"] as? String)?.toInt() ?: return + val type = GraveType.values()[graveData] + GraveController.updateGraveType(player, type) } fun parseAppearance() { diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaver.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaver.kt index ca70c7d84..da39490e5 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaver.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/PlayerSaver.kt @@ -39,7 +39,6 @@ class PlayerSaver (val player: Player){ saveAppearance(saveFile) saveSpellbook(saveFile) saveVarps(saveFile) - saveGraveType(saveFile) saveSavedData(saveFile) saveAutocast(saveFile) saveConfigs(saveFile) @@ -517,10 +516,6 @@ class PlayerSaver (val player: Player){ root.put("activityData",activityData) } - fun saveGraveType(root: JSONObject){ - root.put("grave_type",player.graveManager.type.ordinal.toString()) - } - fun saveSpellbook(root: JSONObject){ root.put("spellbook",player.spellBookManager.spellBook.toString()) } diff --git a/Server/src/main/kotlin/rs09/game/world/GameWorld.kt b/Server/src/main/kotlin/rs09/game/world/GameWorld.kt index 5f1fd4f2a..9bd6ff4fb 100644 --- a/Server/src/main/kotlin/rs09/game/world/GameWorld.kt +++ b/Server/src/main/kotlin/rs09/game/world/GameWorld.kt @@ -14,6 +14,7 @@ import core.game.world.map.RegionManager import core.plugin.CorePluginTypes.StartupPlugin import core.tools.RandomFunction import rs09.ServerConstants +import rs09.ServerStore import rs09.auth.AuthProvider import rs09.game.system.Auth import rs09.game.system.SystemLogger @@ -169,7 +170,9 @@ object GameWorld { ConfigParser().prePlugin() ClassScanner.scanClasspath() ClassScanner.loadPureInterfaces() - worldPersists.forEach { it.parse() } + val s = worldPersists.filterIsInstance().first() + s.parse() + worldPersists.filter { it !is ServerStore }.forEach { it.parse() } ClassScanner.loadSideEffectfulPlugins() configParser.postPlugin() startupListeners.forEach { it.startup() } diff --git a/Server/src/main/kotlin/rs09/game/world/PulseRunner.kt b/Server/src/main/kotlin/rs09/game/world/PulseRunner.kt index 242cfd1e6..cb154b10b 100644 --- a/Server/src/main/kotlin/rs09/game/world/PulseRunner.kt +++ b/Server/src/main/kotlin/rs09/game/world/PulseRunner.kt @@ -17,7 +17,7 @@ class PulseRunner { fun updateAll() { val pulseCount = pulses.size - for (i in 0..pulseCount) { + for (i in 0 until pulseCount) { val pulse = pulses.take() val elapsedTime = measure { diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt index ec337bc91..07128d52d 100644 --- a/Server/src/test/kotlin/TestUtils.kt +++ b/Server/src/test/kotlin/TestUtils.kt @@ -3,6 +3,7 @@ import core.cache.crypto.ISAACCipher import core.cache.crypto.ISAACPair import core.game.node.entity.player.Player import core.game.node.entity.player.info.PlayerDetails +import core.game.node.entity.player.info.Rights import core.game.node.entity.player.link.IronmanMode import core.game.node.item.Item import core.net.IoSession @@ -20,12 +21,16 @@ import rs09.game.world.update.UpdateSequence import java.nio.ByteBuffer object TestUtils { - fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE): Player { + var uidCounter = 0 + + fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE, rights: Rights = Rights.ADMINISTRATOR): Player { val p = MockPlayer(name) p.ironmanManager.mode = ironman + p.details.accountInfo.uid = uidCounter++ Repository.addPlayer(p) //Update sequence has a separate list of players for some reason... UpdateSequence.renderablePlayers.add(p) + p.details.rights = rights return p } diff --git a/Server/src/test/kotlin/content/DeathTests.kt b/Server/src/test/kotlin/content/DeathTests.kt new file mode 100644 index 000000000..ea7f54dde --- /dev/null +++ b/Server/src/test/kotlin/content/DeathTests.kt @@ -0,0 +1,313 @@ +package content + +import TestUtils +import api.asItem +import core.game.content.global.action.DropItemHandler +import core.game.node.entity.combat.ImpactHandler +import core.game.node.entity.player.info.Rights +import core.game.node.entity.player.link.IronmanMode +import core.game.world.map.Location +import org.junit.Assert +import org.junit.BeforeClass +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import org.rs09.consts.Items +import rs09.game.node.entity.player.graves.GraveType +import rs09.game.node.entity.player.graves.GraveController +import rs09.game.world.GameWorld +import rs09.tools.secondsToTicks +class DeathTests { + init { + //explicitly register the GraveController as a tick listener because tests don't run reflection + TestUtils.preTestSetup() // need cache parsed to properly evaluate item values + GameWorld.tickListeners.add(GraveController()) + } + //Grave requirements source: https://runescape.wiki/w/Gravestone?oldid=854455 + + @Test fun graveUtilsProduceGraveShouldProduceCorrectGrave() { + val type = GraveType.MEM_PLAQUE + val grave = GraveController.produceGrave(type) + + Assertions.assertEquals(type, grave.type) + } + + @Test fun graveInitializedWithItemsShouldInitializeCorrectly() { + val inventory = arrayOf( + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.YELLOW_BEAD_1472.asItem() + ) + val player = TestUtils.getMockPlayer("gravetest", IronmanMode.NONE, Rights.REGULAR_PLAYER) + + val grave = GraveController.produceGrave(GraveType.MEM_PLAQUE) + grave.initialize(player, Location.create(0,0,0), inventory) + + for (item in grave.getItems()) { + Assertions.assertEquals(player, item.dropper) + Assertions.assertEquals(player.details.uid, item.dropperUid) + Assertions.assertEquals( + GameWorld.ticks + secondsToTicks(GraveType.MEM_PLAQUE.durationMinutes * 60), + item.decayTime + ) + Assertions.assertEquals(true, item.isRemainPrivate) + Assertions.assertEquals(true, item.id in inventory.map { it.id }.toIntArray()) + Assertions.assertEquals(true, grave.isActive) + } + } + + @Test fun graveInitializedWithNoItemsShouldNotSpawn() { + val grave = GraveController.produceGrave(GraveType.MEM_PLAQUE) + val p = TestUtils.getMockPlayer("gravetest") + + grave.initialize(p, Location.create(0,0,0), arrayOf()) + Assertions.assertEquals(false, grave.isActive) + } + + @Test fun graveInitializedWithEctophialAndPouchesShouldNotKeepThem() { + val inventory = arrayOf( + Items.ECTOPHIAL_4251.asItem(), + Items.SMALL_POUCH_5509.asItem(), + Items.MEDIUM_POUCH_5510.asItem(), + Items.MEDIUM_POUCH_5511.asItem(), + Items.LARGE_POUCH_5512.asItem(), + Items.LARGE_POUCH_5513.asItem(), + Items.GIANT_POUCH_5514.asItem(), + Items.GIANT_POUCH_5515.asItem() + ) + val p = TestUtils.getMockPlayer("gravetest") + val grave = GraveController.produceGrave(GraveType.MEM_PLAQUE) + grave.initialize(p, Location.create(0,0,0), inventory) + + Assertions.assertEquals(true, grave.getItems().isEmpty()) + Assertions.assertEquals(false, grave.isActive) + } + + @Test fun graveInitializedWithDroppableUntradablesShouldKeepThem() { + val inventory = arrayOf( + Items.FIRE_CAPE_6570.asItem(), + Items.RUNE_DEFENDER_8850.asItem() + ) + val p = TestUtils.getMockPlayer("gravetest") + val grave = GraveController.produceGrave(GraveType.MEM_PLAQUE) + grave.initialize(p, Location.create(0,0,0), inventory) + + Assertions.assertEquals(2, grave.getItems().size) + Assertions.assertEquals(true, grave.isActive) + } + + @Test fun graveInitializedWithDestroyableItemsShouldNotKeepThem() { + val inventory = arrayOf( + Items.HOLY_GRAIL_19.asItem() + ) + val p = TestUtils.getMockPlayer("gravetest") + val grave = GraveController.produceGrave(GraveType.MEM_PLAQUE) + grave.initialize(p, Location.create(0,0,0), inventory) + + Assertions.assertEquals(0, grave.getItems().size) + Assertions.assertEquals(false, grave.isActive) + } + + @Test fun graveInitializedWithReleasableItemsShouldNotKeepThem() { + val inventory = arrayOf( + Items.CHINCHOMPA_10033.asItem(), + Items.CHINCHOMPA_9976.asItem(), + Items.BABY_IMPLING_JAR_11238.asItem() + ) + val p = TestUtils.getMockPlayer("gravetest") + val grave = GraveController.produceGrave(GraveType.MEM_PLAQUE) + grave.initialize(p, Location.create(0,0,0), inventory) + + Assertions.assertEquals(0, grave.getItems().size) + Assertions.assertEquals(false, grave.isActive) + } + + @Test fun graveInitializedWithItemThatHasDropTransformShouldContainTransformedItem() { + //We actually don't have any items that have this implemented yet, but we should test it once we do. + } + + @Test fun graveShouldSerializeAndDeserializeFromJsonCorrectly() { + val inventory = arrayOf( + Items.BRONZE_2H_SWORD_1307.asItem(), + Items.BRONZE_AXE_1351.asItem() + ) + + val startTime = GameWorld.ticks + val p = TestUtils.getMockPlayer("gravetest") + val grave = GraveController.produceGrave(GraveType.MEM_PLAQUE) + grave.initialize(p, Location.create(0,0,0), inventory) + + TestUtils.advanceTicks(30, false) + val expectedTicksRemaining = secondsToTicks(GraveType.MEM_PLAQUE.durationMinutes * 60) - (GameWorld.ticks - startTime) + Assertions.assertEquals(expectedTicksRemaining, grave.ticksRemaining) + + GraveController.serializeToServerStore() + GraveController.activeGraves.remove(p.details.uid) + GraveController.deserializeFromServerStore() + + val newGrave = GraveController.activeGraves[p.details.uid] + Assertions.assertNotNull(newGrave) + Assertions.assertEquals(expectedTicksRemaining, newGrave?.ticksRemaining ?: -1) + Assertions.assertEquals(2, newGrave?.getItems()?.size ?: -1) + + val expectedItemDecayTick = GameWorld.ticks + expectedTicksRemaining + Assertions.assertEquals(expectedItemDecayTick, newGrave?.getItems()?.get(0)?.decayTime ?: -1) + Assertions.assertEquals(true, newGrave?.isActive) + } + + @Test fun regularDeathShouldSpawnGraveWithItems() { + val inventory = arrayOf( + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), //none of these should be in the grave due to having higher value + Items.YELLOW_BEAD_1472.asItem(), + Items.RED_BEAD_1470.asItem() + ) + val p = TestUtils.getMockPlayer("gravetester", IronmanMode.NONE, Rights.REGULAR_PLAYER) + p.location = Location.create(0,0,0) + p.setAttribute("tutorial:complete", true) + + for (item in inventory) + p.inventory.add(item) + + p.finalizeDeath(null) + + val grave = GraveController.activeGraves[p.details.uid] + Assertions.assertNotNull(grave) + Assertions.assertEquals(2, grave?.getItems()?.size ?: -1) + Assertions.assertEquals(false, grave?.getItems()?.map { it.id }?.contains(Items.RUNE_SCIMITAR_1333)) + } + + @Test fun skulledDeathShouldNotSpawnGrave() { + val inventory = arrayOf( + Items.ABYSSAL_WHIP_4151.asItem(), + Items.ABYSSAL_WHIP_4151.asItem(), + Items.ABYSSAL_WHIP_4151.asItem(), + Items.ABYSSAL_WHIP_4151.asItem(), + Items.ABYSSAL_WHIP_4151.asItem() + ) + val p = TestUtils.getMockPlayer("gravetester", IronmanMode.NONE, Rights.REGULAR_PLAYER) + p.location = Location.create(0,0,0) + p.setAttribute("tutorial:complete", true) + + for (item in inventory) + p.inventory.add(item) + + p.skullManager.isSkulled = true + p.finalizeDeath(null) + + val grave = GraveController.activeGraves[p.details.uid] + Assertions.assertNull(grave) + } + + @Test fun creatingNewGraveWithGraveAlreadyActiveShouldDestroyOldGrave() { + val inventory1 = arrayOf( + Items.RUNE_SCIMITAR_1333.asItem() + ) + val grave1 = GraveController.produceGrave(GraveType.MEM_PLAQUE) + val p = TestUtils.getMockPlayer("gravetester") + grave1.initialize(p, Location.create(0,0,0), inventory1) + + Assertions.assertEquals(true, grave1.isActive) + Assertions.assertEquals(Items.RUNE_SCIMITAR_1333, GraveController.activeGraves[p.details.uid]?.getItems()?.getOrNull(0)?.id ?: -1) + + val inventory2 = arrayOf( + Items.ABYSSAL_WHIP_4151.asItem() + ) + val grave2 = GraveController.produceGrave(GraveType.MEM_PLAQUE) + grave2.initialize(p, Location.create(0,0,0), inventory2) + + Assertions.assertEquals(false, grave1.isActive) + Assertions.assertEquals(true, grave2.isActive) + Assertions.assertEquals(Items.ABYSSAL_WHIP_4151, GraveController.activeGraves[p.details.uid]?.getItems()?.getOrNull(0)?.id ?: -1) + } + + @Test fun deathWithOnly3ItemsShouldNotProduceAGrave() { + val inventory = arrayOf( + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem() + ) + val p = TestUtils.getMockPlayer("gtester", IronmanMode.NONE, Rights.REGULAR_PLAYER) + p.location = Location.create(0,0,0) + p.setAttribute("tutorial:complete", true) + + for (item in inventory) + p.inventory.add(item) + + p.finalizeDeath(null) + + Assertions.assertNull(GraveController.activeGraves[p.details.uid]) + } + + @Test fun deathInsideWildernessShouldNotProduceAGrave() { + val inventory = arrayOf( + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem() + ) + val p = TestUtils.getMockPlayer("tester3333", IronmanMode.NONE, Rights.REGULAR_PLAYER) + p.skullManager.isWilderness = true + + for (item in inventory) { + p.inventory.add(item) + } + + p.finalizeDeath(null) + + Assertions.assertNull(GraveController.activeGraves[p.details.uid]) + } + + @Test fun deathWithRFDGlovesShouldKeepRFDGloves() { + val inventory = arrayOf( + Items.GLOVES_7453.asItem(), + Items.GLOVES_7454.asItem(), + Items.GLOVES_7455.asItem(), + Items.GLOVES_7456.asItem(), + Items.GLOVES_7457.asItem(), + Items.GLOVES_7458.asItem(), + Items.GLOVES_7459.asItem(), + Items.GLOVES_7460.asItem(), + Items.GLOVES_7461.asItem(), + Items.GLOVES_7462.asItem() + ) + val p = TestUtils.getMockPlayer("glovetest", IronmanMode.NONE, Rights.REGULAR_PLAYER) + + for(item in inventory) + p.inventory.add(item) + + p.finalizeDeath(null) + + val g = GraveController.activeGraves[p.details.uid] + Assertions.assertNotNull(g) + Assertions.assertEquals(7, g?.getItems()?.size ?: -1) + } + + @Test fun shouldNotBeAbleToDropItemOnGrave() { + val inventory = arrayOf( + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem(), + Items.RUNE_SCIMITAR_1333.asItem() + ) + val p = TestUtils.getMockPlayer("droptest", IronmanMode.NONE, Rights.REGULAR_PLAYER) + + for (item in inventory) + p.inventory.add(item) + + p.finalizeDeath(null) + p.inventory.add(Items.RUNE_SCIMITAR_1333.asItem()) + + val g = GraveController.activeGraves[p.details.uid] + + Assertions.assertNotNull(g) + Assertions.assertEquals(p.location, g?.location) + + val canDrop = DropItemHandler.drop(p, p.inventory[0]) + Assertions.assertEquals(false, canDrop) + } +} \ No newline at end of file