From a3667f10e39a18cf5e4bae19065dd5e05ab486e3 Mon Sep 17 00:00:00 2001 From: Lucwousin Date: Sat, 26 Oct 2019 21:31:00 +0200 Subject: [PATCH] deathindicator: add permabones (#1829) * Add api support for faking ground items and add permabones * Remove unused method * fixed loading/keeping loaded, fixed calendar thingy, probably forgot something * Actually commit the calendar tsuff --- .../java/net/runelite/api/ItemDefinition.java | 13 + .../src/main/java/net/runelite/api/Scene.java | 12 + .../net/runelite/api/events/MenuOpened.java | 24 +- .../client/plugins/deathindicator/Bone.java | 42 ++++ .../client/plugins/deathindicator/Bones.java | 200 +++++++++++++++ .../deathindicator/DeathIndicatorConfig.java | 10 + .../deathindicator/DeathIndicatorPlugin.java | 233 ++++++++++++++++-- .../net/runelite/client/util/MiscUtils.java | 84 +++++++ .../mixins/RSItemDefinitionMixin.java | 25 ++ .../net/runelite/mixins/RSSceneMixin.java | 71 ++++++ .../java/net/runelite/rs/api/RSClient.java | 11 +- .../net/runelite/rs/api/RSItemDefinition.java | 11 + .../java/net/runelite/rs/api/RSNodeDeque.java | 9 + .../java/net/runelite/rs/api/RSScene.java | 3 + 14 files changed, 728 insertions(+), 20 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bone.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bones.java diff --git a/runelite-api/src/main/java/net/runelite/api/ItemDefinition.java b/runelite-api/src/main/java/net/runelite/api/ItemDefinition.java index c955574543..891fdeec9c 100644 --- a/runelite-api/src/main/java/net/runelite/api/ItemDefinition.java +++ b/runelite-api/src/main/java/net/runelite/api/ItemDefinition.java @@ -12,6 +12,11 @@ public interface ItemDefinition */ String getName(); + /** + * Sets the items name. + */ + void setName(String name); + /** * Gets the items ID. * @@ -86,6 +91,7 @@ public interface ItemDefinition * Returns whether or not the item can be sold on the grand exchange. */ boolean isTradeable(); + void setTradeable(boolean yes); /** * Gets an array of possible right-click menu actions the item @@ -114,4 +120,11 @@ public interface ItemDefinition * default value. */ void resetShiftClickActionIndex(); + + /** + * With this you can make certain (ground) items look like different ones. + * + * @param id The itemID of the item with desired model + */ + void setModelOverride(int id); } diff --git a/runelite-api/src/main/java/net/runelite/api/Scene.java b/runelite-api/src/main/java/net/runelite/api/Scene.java index 53cc8fcb15..5b7460340b 100644 --- a/runelite-api/src/main/java/net/runelite/api/Scene.java +++ b/runelite-api/src/main/java/net/runelite/api/Scene.java @@ -24,6 +24,8 @@ */ package net.runelite.api; +import net.runelite.api.coords.WorldPoint; + /** * Represents the entire 3D scene */ @@ -36,6 +38,16 @@ public interface Scene */ Tile[][][] getTiles(); + /** + * Adds an item to the scene + */ + void addItem(int id, int quantity, WorldPoint point); + + /** + * Removes an item from the scene + */ + void removeItem(int id, int quantity, WorldPoint point); + int getDrawDistance(); void setDrawDistance(int drawDistance); } diff --git a/runelite-api/src/main/java/net/runelite/api/events/MenuOpened.java b/runelite-api/src/main/java/net/runelite/api/events/MenuOpened.java index 221c8b9d0d..ee31b0ad04 100644 --- a/runelite-api/src/main/java/net/runelite/api/events/MenuOpened.java +++ b/runelite-api/src/main/java/net/runelite/api/events/MenuOpened.java @@ -24,6 +24,7 @@ */ package net.runelite.api.events; +import java.util.Iterator; import lombok.AccessLevel; import lombok.Setter; import net.runelite.api.MenuEntry; @@ -33,7 +34,7 @@ import lombok.Data; * An event where a menu has been opened. */ @Data -public class MenuOpened implements Event +public class MenuOpened implements Event, Iterable { /** * This should be set to true if anything about the menu @@ -70,4 +71,25 @@ public class MenuOpened implements Event { this.modified = true; } + + @Override + public Iterator iterator() + { + return new Iterator() + { + int index = 0; + + @Override + public boolean hasNext() + { + return index < menuEntries.length; + } + + @Override + public MenuEntry next() + { + return menuEntries[index++]; + } + }; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bone.java b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bone.java new file mode 100644 index 0000000000..28daed468e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bone.java @@ -0,0 +1,42 @@ +package net.runelite.client.plugins.deathindicator; + +import java.time.Duration; +import java.time.Instant; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Scene; +import net.runelite.api.coords.WorldPoint; +import static net.runelite.client.plugins.deathindicator.DeathIndicatorPlugin.HIJACKED_ITEMID; +import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.MiscUtils; + +@EqualsAndHashCode +@Getter +@Setter +class Bone +{ + private String name; + private WorldPoint loc; + private Instant time; + + void addToScene(Scene scene) + { + scene.addItem(HIJACKED_ITEMID, 1, loc); + } + + void removeFromScene(Scene scene) + { + scene.removeItem(HIJACKED_ITEMID, 1, loc); + } + + String getName() + { + return ColorUtil.colorStartTag(0xff9040) + "Bones (" + name + ")"; + } + + String getExamine() + { + return name + " died here " + MiscUtils.formatTimeAgo(Duration.between(time, Instant.now())); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bones.java b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bones.java new file mode 100644 index 0000000000..7b561dd63b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/Bones.java @@ -0,0 +1,200 @@ +package net.runelite.client.plugins.deathindicator; + +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.Scene; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.config.ConfigManager; +import static net.runelite.http.api.RuneLiteAPI.GSON; + +@Slf4j +public class Bones +{ + private static final String CONFIG_GROUP = "deathIndicator"; + private static final String BONES_PREFIX = "bones_"; + + private ImmutableMap>> map; + private boolean changed = false; + + void init(Client client, ConfigManager configManager) + { + // Clone is important here as the normal array changes + int[] regions = client.getMapRegions().clone(); + Bone[][] bones = getBones(configManager, regions); + if (log.isDebugEnabled()) + { + log.debug("Regions are now {}", Arrays.toString(regions)); + + int n = 0; + for (Bone[] ar : bones) + { + n += ar.length; + } + log.debug("Loaded {} Bones", n); + } + + initMap(regions, bones); + + showBones(client.getScene()); + } + + private Bone[][] getBones(ConfigManager configManager, int[] regions) + { + Bone[][] bones = new Bone[regions.length][]; + + for (int i = 0; i < regions.length; i++) + { + int region = regions[i]; + bones[i] = getBones(configManager, region); + } + + return bones; + } + + private Bone[] getBones(ConfigManager configManager, int regionId) + { + String json = configManager.getConfiguration(CONFIG_GROUP, BONES_PREFIX + regionId); + if (json == null) + { + return new Bone[0]; + } + + return GSON.fromJson(json, Bone[].class); + } + + @SuppressWarnings("unchecked") + private void initMap(int[] regions, Bone[][] bones) + { + ImmutableMap.Builder>> builder = ImmutableMap.builder(); + + for (int i = 0; i < regions.length; i++) + { + int region = regions[i]; + Bone[] boneA = bones[i]; + if (boneA.length == 0) + { + builder.put(region, Collections.EMPTY_MAP); + continue; + } + + Map> map = new HashMap(boneA.length); + for (Bone b : boneA) + { + List list = map.computeIfAbsent(b.getLoc(), wp -> new ArrayList<>()); + list.add(b); + } + + builder.put(region, map); + } + + this.map = builder.build(); + } + + private void showBones(Scene scene) + { + this.forEach(bone -> bone.addToScene(scene)); + } + + void save(ConfigManager configManager) + { + if (this.map == null || !changed) + { + this.changed = false; + return; + } + + for (Map.Entry>> entry : this.map.entrySet()) + { + final String key = BONES_PREFIX + entry.getKey(); + final Map> map = entry.getValue(); + if (map.size() == 0) + { + configManager.unsetConfiguration(CONFIG_GROUP, key); + continue; + } + + List list = new ArrayList<>(map.values().size()); + for (List lb : map.values()) + { + list.addAll(lb); + } + + String val = GSON.toJson(list.toArray(new Bone[0])); + configManager.setConfiguration(CONFIG_GROUP, key, val); + } + + this.changed = false; + } + + public boolean add(Bone bone) + { + if (this.map == null) + { + return false; + } + + this.changed = true; + final int region = bone.getLoc().getRegionID(); + final Map> map = this.map.get(region); + final List list = map.computeIfAbsent(bone.getLoc(), wp -> new ArrayList<>()); + list.add(bone); + return true; + } + + public void remove(Bone bone) + { + this.changed = true; + final int region = bone.getLoc().getRegionID(); + final Map> map = this.map.get(region); + final List list = map.get(bone.getLoc()); + list.remove(bone); + } + + public void clear(Scene scene) + { + if (map == null) + { + return; + } + + this.forEach(bone -> bone.removeFromScene(scene)); + } + + public Bone get(WorldPoint point, int i) + { + return get(point).get(i); + } + + public List get(WorldPoint point) + { + final int reg = point.getRegionID(); + final Map> map = this.map.get(reg); + if (map == null) + { + return null; + } + return map.get(point); + } + + private void forEach(Consumer consumer) + { + for (Map> map : this.map.values()) + { + for (Map.Entry> entry : map.entrySet()) + { + for (Bone bone : entry.getValue()) + { + consumer.accept(bone); + } + } + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorConfig.java index e35b7cc93f..f03ff689d9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorConfig.java @@ -152,4 +152,14 @@ public interface DeathIndicatorConfig extends Config description = "" ) void timeOfDeath(Instant timeOfDeath); + + @ConfigItem( + keyName = "permaBones", + name = "Permanent bones", + description = "Show right clickable bones with the name of who died permanently, after seeing someone die" + ) + default boolean permaBones() + { + return false; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorPlugin.java index 194500c4cc..33ec854e8f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/deathindicator/DeathIndicatorPlugin.java @@ -30,18 +30,32 @@ import java.awt.image.BufferedImage; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; import net.runelite.api.Client; import net.runelite.api.GameState; +import net.runelite.api.ItemDefinition; import net.runelite.api.ItemID; +import net.runelite.api.MenuEntry; +import net.runelite.api.MenuOpcode; +import net.runelite.api.Player; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GameTick; +import net.runelite.api.events.ItemDespawned; import net.runelite.api.events.LocalPlayerDeath; +import net.runelite.api.events.MenuEntryAdded; +import net.runelite.api.events.MenuOpened; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.events.PlayerDeath; +import net.runelite.api.events.PostItemDefinition; +import net.runelite.api.util.Text; +import net.runelite.client.callback.ClientThread; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.EventBus; import net.runelite.client.game.ItemManager; @@ -61,6 +75,10 @@ import net.runelite.client.util.ImageUtil; @Slf4j public class DeathIndicatorPlugin extends Plugin { + private static final Object BONES = new Object(); + // A random number, that jagex probably won't actually use in the near future + static final int HIJACKED_ITEMID = 0x69696969; + private static final Set RESPAWN_REGIONS = ImmutableSet.of( 12850, // Lumbridge 11828, // Falador @@ -88,6 +106,14 @@ public class DeathIndicatorPlugin extends Plugin @Inject private EventBus eventBus; + @Inject + private ConfigManager configManager; + + @Inject + private ClientThread clientThread; + + private final Bones bones = new Bones(); + private BufferedImage mapArrow; private Timer deathTimer; @@ -95,7 +121,7 @@ public class DeathIndicatorPlugin extends Plugin private WorldPoint lastDeath; private Instant lastDeathTime; private int lastDeathWorld; - + private int despawnIdx = 0; @Provides DeathIndicatorConfig deathIndicatorConfig(ConfigManager configManager) { @@ -129,12 +155,18 @@ public class DeathIndicatorPlugin extends Plugin worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); worldMapPointManager.add(new DeathWorldMapPoint(new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane()), this)); } + + if (config.permaBones() && client.getGameState() == GameState.LOGGED_IN) + { + clientThread.invokeLater(this::initBones); + } } @Override protected void shutDown() { eventBus.unregister(this); + eventBus.unregister(BONES); if (client.hasHintArrow()) { @@ -148,6 +180,24 @@ public class DeathIndicatorPlugin extends Plugin } worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + + clientThread.invokeLater(this::clearBones); + saveBones(); + } + + private void initBones() + { + bones.init(client, configManager); + } + + private void saveBones() + { + bones.save(configManager); + } + + private void clearBones() + { + bones.clear(client.getScene()); } private void addSubscriptions() @@ -156,6 +206,100 @@ public class DeathIndicatorPlugin extends Plugin eventBus.subscribe(LocalPlayerDeath.class, this, this::onLocalPlayerDeath); eventBus.subscribe(GameTick.class, this, this::onGameTick); eventBus.subscribe(GameStateChanged.class, this, this::onGameStateChanged); + if (config.permaBones()) + { + addBoneSubs(); + } + } + + private void addBoneSubs() + { + eventBus.subscribe(ItemDespawned.class, BONES, this::onItemDespawn); + eventBus.subscribe(PlayerDeath.class, BONES, this::onPlayerDeath); + eventBus.subscribe(MenuEntryAdded.class, BONES, this::onMenuEntryAdded); + eventBus.subscribe(MenuOptionClicked.class, BONES, this::onMenuOptionClicked); + eventBus.subscribe(MenuOpened.class, BONES, this::onMenuOpened); + eventBus.subscribe(PostItemDefinition.class, BONES, this::onPostItemDefinition); + } + + private void onPostItemDefinition(PostItemDefinition def) + { + ItemDefinition itemDef = def.getItemDefinition(); + if (itemDef.getId() == HIJACKED_ITEMID) + { + itemDef.setModelOverride(ItemID.BONES); + itemDef.setName("Bones"); + // This is so never hide untradeables doesn't not hide it + itemDef.setTradeable(true); + } + } + + private void onPlayerDeath(PlayerDeath death) + { + Player p = death.getPlayer(); + Bone b = new Bone(); + + b.setName(Text.sanitize(p.getName())); + b.setTime(Instant.now()); + b.setLoc(p.getWorldLocation()); + + while (!bones.add(b)) + { + initBones(); + } + + b.addToScene(client.getScene()); + } + + private void onMenuEntryAdded(MenuEntryAdded event) + { + if (event.getIdentifier() == HIJACKED_ITEMID) + { + if (event.getOpcode() == MenuOpcode.GROUND_ITEM_THIRD_OPTION.getId()) + { + client.setMenuOptionCount(client.getMenuOptionCount() - 1); + } + } + } + + private void onMenuOpened(MenuOpened event) + { + int idx = 0; + + for (MenuEntry entry : event) + { + if (entry.getIdentifier() != HIJACKED_ITEMID) + { + continue; + } + + // Only entries with appropriate identifier here will be examine so that's easy + // Add idx to id field, so we can find that back from clicked event + entry.setIdentifier(HIJACKED_ITEMID + idx); + + Bone bone = bones.get( + WorldPoint.fromScene(client, entry.getParam0(), entry.getParam1(), client.getPlane()), + idx++ + ); + + entry.setTarget(bone.getName()); + event.setModified(); + } + } + + private void onMenuOptionClicked(MenuOptionClicked event) + { + if (event.getIdentifier() >= HIJACKED_ITEMID + && event.getOpcode() == MenuOpcode.EXAMINE_ITEM_GROUND.getId()) + { + Bone b = bones.get( + WorldPoint.fromScene(client, event.getParam0(), event.getParam1(), client.getPlane()), + event.getIdentifier() - HIJACKED_ITEMID + ); + + client.addChatMessage(ChatMessageType.ITEM_EXAMINE, "", b.getExamine(), ""); + event.consume(); + } } private void onLocalPlayerDeath(LocalPlayerDeath death) @@ -238,6 +382,29 @@ public class DeathIndicatorPlugin extends Plugin { if (event.getGroup().equals("deathIndicator")) { + if ("permaBones".equals(event.getKey())) + { + if (config.permaBones()) + { + addBoneSubs(); + + if (client.getGameState() == GameState.LOGGED_IN) + { + clientThread.invokeLater(this::initBones); + } + } + else + { + eventBus.unregister(BONES); + + if (client.getGameState() == GameState.LOGGED_IN) + { + clientThread.invokeLater(this::clearBones); + saveBones(); + } + } + return; + } if (!config.showDeathHintArrow() && hasDied()) { client.clearHintArrow(); @@ -267,32 +434,62 @@ public class DeathIndicatorPlugin extends Plugin private void onGameStateChanged(GameStateChanged event) { - if (!hasDied()) + switch (event.getGameState()) { - return; - } - - if (event.getGameState() == GameState.LOGGED_IN) - { - if (client.getWorld() == config.deathWorld()) - { - WorldPoint deathPoint = new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane()); - - if (config.showDeathHintArrow()) + case LOADING: + clearBones(); + saveBones(); + break; + case LOGGED_IN: + if (config.permaBones()) { - client.setHintArrow(deathPoint); + initBones(); } - if (config.showDeathOnWorldMap()) + if (!hasDied()) + { + return; + } + + if (client.getWorld() == config.deathWorld()) + { + WorldPoint deathPoint = new WorldPoint(config.deathLocationX(), config.deathLocationY(), config.deathLocationPlane()); + + if (config.showDeathHintArrow()) + { + client.setHintArrow(deathPoint); + } + + if (config.showDeathOnWorldMap()) + { + worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + worldMapPointManager.add(new DeathWorldMapPoint(deathPoint, this)); + } + } + else { worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); - worldMapPointManager.add(new DeathWorldMapPoint(deathPoint, this)); } - } - else + break; + } + + } + + private void onItemDespawn(ItemDespawned event) + { + if (event.getItem().getId() == HIJACKED_ITEMID) + { + List list = bones.get(event.getTile().getWorldLocation()); + if (list == null) { - worldMapPointManager.removeIf(DeathWorldMapPoint.class::isInstance); + return; } + if (list.size() <= despawnIdx) + { + despawnIdx = 0; + } + Bone bone = list.get(despawnIdx++); + bone.addToScene(client.getScene()); } } diff --git a/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java b/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java index 647a411cdf..29e7d7b93f 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java +++ b/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java @@ -1,6 +1,8 @@ package net.runelite.client.util; import java.awt.Polygon; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import net.runelite.api.Client; import net.runelite.api.Player; import net.runelite.api.WorldType; @@ -16,6 +18,17 @@ public class MiscUtils private static final Polygon abovePoly = new Polygon(abovePointsX, abovePointsY, abovePointsX.length); private static final Polygon belowPoly = new Polygon(belowPointsX, belowPointsY, belowPointsX.length); + private static final ChronoUnit[] ORDERED_CHRONOS = new ChronoUnit[] + { + ChronoUnit.YEARS, + ChronoUnit.MONTHS, + ChronoUnit.WEEKS, + ChronoUnit.DAYS, + ChronoUnit.HOURS, + ChronoUnit.MINUTES, + ChronoUnit.SECONDS + }; + //test replacement so private for now private static boolean inWildy(WorldPoint point) { @@ -86,4 +99,75 @@ public class MiscUtils //return getWildernessLevelFrom(client, localPlayer.getWorldLocation()) > 0; } + + public static String formatTimeAgo(Duration dur) + { + long dA = 0, dB = 0, rm; + ChronoUnit cA = null, cB = null; + for (int i = 0; i < ORDERED_CHRONOS.length; i++) + { + cA = ORDERED_CHRONOS[i]; + dA = dur.getSeconds() / cA.getDuration().getSeconds(); + rm = dur.getSeconds() % cA.getDuration().getSeconds(); + if (dA <= 0) + { + cA = null; + continue; + } + + if (i + 1 < ORDERED_CHRONOS.length) + { + cB = ORDERED_CHRONOS[i + 1]; + dB = rm / cB.getDuration().getSeconds(); + + if (dB <= 0) + { + cB = null; + } + } + + break; + } + + if (cA == null) + { + return "just now."; + } + + String str = formatUnit(cA, dA); + + if (cB != null) + { + str += " and " + formatUnit(cB, dB); + } + + return str + " ago."; + } + + private static String formatUnit(ChronoUnit chrono, long val) + { + boolean multiple = val != 1; + String str; + if (multiple) + { + str = val + " "; + } + else + { + str = "a" + (chrono == ChronoUnit.HOURS ? "n " : " "); + } + str += chrono.name().toLowerCase(); + if (!multiple) + { + if (str.charAt(str.length() - 1) == 's') + { + str = str.substring(0, str.length() - 1); + } + } + else if (str.charAt(str.length() - 1) != 's') + { + str += "s"; + } + return str; + } } diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSItemDefinitionMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSItemDefinitionMixin.java index 69d0a0cf27..a5f5ac535f 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSItemDefinitionMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSItemDefinitionMixin.java @@ -9,6 +9,7 @@ import net.runelite.api.mixins.Replace; import net.runelite.api.mixins.Shadow; import net.runelite.rs.api.RSClient; import net.runelite.rs.api.RSItemDefinition; +import net.runelite.rs.api.RSModel; @Mixin(RSItemDefinition.class) public abstract class RSItemDefinitionMixin implements RSItemDefinition @@ -21,6 +22,16 @@ public abstract class RSItemDefinitionMixin implements RSItemDefinition @Inject private int shiftClickActionIndex = DEFAULT_CUSTOM_SHIFT_CLICK_INDEX; + @Inject + private int modelOverride = -1; + + @Override + @Inject + public void setModelOverride(int id) + { + modelOverride = id; + } + @Inject RSItemDefinitionMixin() { @@ -64,4 +75,18 @@ public abstract class RSItemDefinitionMixin implements RSItemDefinition event.setItemDefinition(this); client.getCallbacks().post(PostItemDefinition.class, event); } + + @Copy("getModel") + public abstract RSModel rs$getModel(int quantity); + + @Replace("getModel") + public RSModel getModel(int quantity) + { + if (modelOverride == -1) + { + return rs$getModel(quantity); + } + + return client.getItemDefinition(modelOverride).getModel(quantity); + } } diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/RSSceneMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/RSSceneMixin.java index f723e632f1..656e86b4cc 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/RSSceneMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/RSSceneMixin.java @@ -29,6 +29,7 @@ import net.runelite.api.Perspective; import net.runelite.api.TileModel; import net.runelite.api.TilePaint; import net.runelite.api.Tile; +import net.runelite.api.coords.WorldPoint; import net.runelite.api.hooks.DrawCallbacks; import net.runelite.api.mixins.Copy; import net.runelite.api.mixins.Inject; @@ -38,6 +39,8 @@ import net.runelite.api.mixins.Shadow; import net.runelite.rs.api.RSBoundaryObject; import net.runelite.rs.api.RSClient; import net.runelite.rs.api.RSFloorDecoration; +import net.runelite.rs.api.RSNodeDeque; +import net.runelite.rs.api.RSTileItem; import net.runelite.rs.api.RSTileItemPile; import net.runelite.rs.api.RSScene; import net.runelite.rs.api.RSTile; @@ -716,4 +719,72 @@ public abstract class RSSceneMixin implements RSScene client.setSelectedSceneTileX(targetX); client.setSelectedSceneTileY(targetY); } + + @Override + @Inject + public void addItem(int id, int quantity, WorldPoint point) + { + final int sceneX = point.getX() - client.getBaseX(); + final int sceneY = point.getY() - client.getBaseY(); + final int plane = point.getPlane(); + + if (sceneX < 0 || sceneY < 0 || sceneX >= 104 || sceneY >= 104) + { + return; + } + + RSTileItem item = client.newTileItem(); + item.setId(id); + item.setQuantity(quantity); + RSNodeDeque[][][] groundItems = client.getGroundItemDeque(); + + if (groundItems[plane][sceneX][sceneY] == null) + { + groundItems[plane][sceneX][sceneY] = client.newNodeDeque(); + } + + groundItems[plane][sceneX][sceneY].addFirst(item); + + if (plane == client.getPlane()) + { + client.updateItemPile(sceneX, sceneY); + } + } + + @Override + @Inject + public void removeItem(int id, int quantity, WorldPoint point) + { + final int sceneX = point.getX() - client.getBaseX(); + final int sceneY = point.getY() - client.getBaseY(); + final int plane = point.getPlane(); + + if (sceneX < 0 || sceneY < 0 || sceneX >= 104 || sceneY >= 104) + { + return; + } + + RSNodeDeque items = client.getGroundItemDeque()[plane][sceneX][sceneY]; + + if (items == null) + { + return; + } + + for (RSTileItem item = (RSTileItem) items.last(); item != null; item = (RSTileItem) items.previous()) + { + if (item.getId() == id && quantity == 1) + { + item.unlink(); + break; + } + } + + if (items.last() == null) + { + client.getGroundItemDeque()[plane][sceneX][sceneY] = null; + } + + client.updateItemPile(sceneX, sceneY); + } } diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java index 5ecdfc616b..b1561e7439 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSClient.java @@ -1100,4 +1100,13 @@ public interface RSClient extends RSGameShell, Client */ @Import("Login_promptCredentials") void promptCredentials(boolean clearPass); -} \ No newline at end of file + + @Construct + RSTileItem newTileItem(); + + @Construct + RSNodeDeque newNodeDeque(); + + @Import("updateItemPile") + void updateItemPile(int localX, int localY); +} diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSItemDefinition.java b/runescape-api/src/main/java/net/runelite/rs/api/RSItemDefinition.java index 609ef5eb45..d7551e9944 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSItemDefinition.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSItemDefinition.java @@ -9,6 +9,10 @@ public interface RSItemDefinition extends ItemDefinition @Override String getName(); + @Import("name") + @Override + void setName(String name); + @Import("id") @Override int getId(); @@ -41,6 +45,10 @@ public interface RSItemDefinition extends ItemDefinition @Override boolean isTradeable(); + @Import("isTradable") + @Override + void setTradeable(boolean yes); + /** * You probably want {@link #isStackable} *

@@ -60,4 +68,7 @@ public interface RSItemDefinition extends ItemDefinition @Import("getShiftClickIndex") @Override int getShiftClickActionIndex(); + + @Import("getModel") + RSModel getModel(int quantity); } diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSNodeDeque.java b/runescape-api/src/main/java/net/runelite/rs/api/RSNodeDeque.java index c903f20f8c..d1686fa635 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSNodeDeque.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSNodeDeque.java @@ -9,4 +9,13 @@ public interface RSNodeDeque @Import("sentinel") RSNode getHead(); + + @Import("last") + RSNode last(); + + @Import("previous") + RSNode previous(); + + @Import("addFirst") + void addFirst(RSNode val); } diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSScene.java b/runescape-api/src/main/java/net/runelite/rs/api/RSScene.java index 0b2705397d..73e306a680 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSScene.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSScene.java @@ -36,4 +36,7 @@ public interface RSScene extends Scene @Import("minPlane") int getMinLevel(); + + @Import("newGroundItemPile") + void newGroundItemPile(int plane, int x, int y, int hash, RSEntity var5, long var6, RSEntity var7, RSEntity var8); }