Graves Rewrite

Rewrote graves from scratch using new systems and abundant unit testing
Fixed a race condition bug that would prevent classes implementing PersistWorld from properly... persisting
Fixed a bug where graves were (still) too generous
Fixed a bug where graves would not persist across server reboots
Fixed a bug where graves would not recognize a relogged player
Fixed numerous other bugs with graves, see the unit tests for complete coverage
Grave system now supports quest requirements
This commit is contained in:
Ceikry 2022-09-17 14:40:25 +00:00 committed by Ryan
parent 95bd5ab4b7
commit f406b8aa98
23 changed files with 948 additions and 797 deletions

View file

@ -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;

View file

@ -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:

View file

@ -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<Object> 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;
}
}

View file

@ -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<GroundItem> 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<GroundItem>) objects[2]);
npc.setType((GraveType) objects[3]);
npc.setDisplay((String) objects[4]);
}
GraveManager.getGraves().put(npc.getOwner(), npc);
return npc;
}
@Override
public Plugin<Object> 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<GroundItem> getItems() {
return items;
}
/**
* Sets the baitems.
* @param items the items to set.
*/
public void setItems(List<GroundItem> items) {
this.items = items;
}
/**
* Handles the interactions of a grave stone.
* @author Vexia
*/
public class GraveStonePlugin extends OptionHandler {
@Override
public Plugin<Object> 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;
}
}
}

View file

@ -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<GroundItem> 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("<col=990000>Because of your current gravestone, you have "+graveManager.getType().getDecay()+" minutes to get your items and", "<col=990000>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

View file

@ -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();

View file

@ -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<String, NPC> 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<GroundItem> 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<String, NPC> getGraves() {
return graves;
}
}

View file

@ -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;
}
}

View file

@ -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 + "]";

View file

@ -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.

View file

@ -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) {

View file

@ -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;

View file

@ -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))
}

View file

@ -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<GroundItem>()
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<Item>) {
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<Item>, 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<GroundItem> {
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"
}
}

View file

@ -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<Int, Grave>()
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<Map.Entry<String, JSONObject>>) {
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<Item>()
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
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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,<br>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,<br>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,<br>pause awhile to remember @name,<br>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,<br>an enlightened servant of Saradomin,<br>perished in this place."),
ZAM_SYMBOL(NPCs.ZAMORAK_SYMBOL_6586, 50000, 4, true, text = "@name,<br>a most bloodthirsty follower of Zamorak,<br>perished in this place."),
GUTH_SYMBOL(NPCs.GUTHIX_SYMBOL_6589, 50000, 4, true, text = "@name,<br>who walked with the Balance of Guthix,<br>perished in this place."),
BAND_SYMBOL(NPCs.BANDOS_SYMBOL_6592, 50000, 4, true, requiredQuest = "Land of the Goblins", text = "@name,<br>a vicious warrior dedicated to Bandos,<br>perished in this place. "),
ARMA_SYMBOL(NPCs.ARMADYL_SYMBOL_6595, 50000, 4, true, requiredQuest = "Temple of Ikov", text = "@name,<br>a follower of the Law of Armadyl,<br>perished in this place."),
ZARO_SYMBOL(NPCs.MEMORIAL_STONE_6598, 50000, 4, true, requiredQuest = "Desert Treasure", text = "@name,<br>servant of the Unknown Power,<br>perished in this place."),
ANGEL_DEATH(NPCs.MEMORIAL_STONE_6601, 500000, 5, true, text = "Ye frail mortals who gaze upon this sight,<br>forget not the fate of @name, once mighty, now<br>surrendered to the inescapable grasp of destiny.<br><i>Requiescat in pace.</i>");
companion object {
val ids = values().fold(ArrayList<Int>()) {list, type ->
list.add(type.npcId)
list.add(type.npcId + 1)
list.add(type.npcId + 2)
list
}.toIntArray()
}
}

View file

@ -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() {

View file

@ -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())
}

View file

@ -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<ServerStore>().first()
s.parse()
worldPersists.filter { it !is ServerStore }.forEach { it.parse() }
ClassScanner.loadSideEffectfulPlugins()
configParser.postPlugin()
startupListeners.forEach { it.startup() }

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}
}