diff --git a/runelite-api/src/main/java/net/runelite/api/Constants.java b/runelite-api/src/main/java/net/runelite/api/Constants.java index ef8dca4dc5a..407831d491b 100644 --- a/runelite-api/src/main/java/net/runelite/api/Constants.java +++ b/runelite-api/src/main/java/net/runelite/api/Constants.java @@ -147,4 +147,8 @@ public class Constants * @see ItemComposition#getPrice */ public static final float HIGH_ALCHEMY_MULTIPLIER = .6f; + + public static final int CLICK_ACTION_NONE = 0; + public static final int CLICK_ACTION_WALK = 1; + public static final int CLICK_ACTION_SET_HEADING = 2; } diff --git a/runelite-api/src/main/java/net/runelite/api/DynamicObject.java b/runelite-api/src/main/java/net/runelite/api/DynamicObject.java index bc0e583f540..a2843b528d1 100644 --- a/runelite-api/src/main/java/net/runelite/api/DynamicObject.java +++ b/runelite-api/src/main/java/net/runelite/api/DynamicObject.java @@ -53,4 +53,10 @@ public interface DynamicObject extends Renderable * @return */ Model getModelZbuf(); + + /** + * The object composition for the model returned by {@link #getModelZbuf()} + * @return + */ + ObjectComposition getRecordedObjectComposition(); } diff --git a/runelite-api/src/main/java/net/runelite/api/MenuAction.java b/runelite-api/src/main/java/net/runelite/api/MenuAction.java index 913d3420e7e..d53e1b96c6d 100644 --- a/runelite-api/src/main/java/net/runelite/api/MenuAction.java +++ b/runelite-api/src/main/java/net/runelite/api/MenuAction.java @@ -246,6 +246,12 @@ public enum MenuAction SET_HEADING(60), + WORLD_ENTITY_FIRST_OPTION(63), + WORLD_ENTITY_SECOND_OPTION(64), + WORLD_ENTITY_THIRD_OPTION(65), + WORLD_ENTITY_FOURTH_OPTION(66), + WORLD_ENTITY_FIFTH_OPTION(67), + /** * RuneLite menu that is a widge. * @see MenuEntry#getWidget() @@ -283,6 +289,8 @@ public enum MenuAction */ CC_OP_LOW_PRIORITY(1007), + EXAMINE_WORLD_ENTITY(1013), + /** * Menu action injected by runelite for its menu items. */ diff --git a/runelite-api/src/main/java/net/runelite/api/Model.java b/runelite-api/src/main/java/net/runelite/api/Model.java index 0b7ec4d73ba..456b82e87f4 100644 --- a/runelite-api/src/main/java/net/runelite/api/Model.java +++ b/runelite-api/src/main/java/net/runelite/api/Model.java @@ -37,6 +37,8 @@ public interface Model extends Mesh, Renderable int[] getFaceColors3(); + short[] getUnlitFaceColors(); + int getSceneId(); void setSceneId(int sceneId); diff --git a/runelite-api/src/main/java/net/runelite/api/TileObject.java b/runelite-api/src/main/java/net/runelite/api/TileObject.java index 055081acb15..a9a1847814e 100644 --- a/runelite-api/src/main/java/net/runelite/api/TileObject.java +++ b/runelite-api/src/main/java/net/runelite/api/TileObject.java @@ -149,4 +149,16 @@ public interface TileObject */ @Nullable Shape getClickbox(); + + /** + * Get the text override for a certain action + */ + @Nullable + String getOpOverride(int index); + + /** + * Gets if an action is shown in the minimenu. If an action is {@code null} it + * will not be shown even if this method returns {@code true} + */ + boolean isOpShown(int index); } diff --git a/runelite-api/src/main/java/net/runelite/api/WorldEntity.java b/runelite-api/src/main/java/net/runelite/api/WorldEntity.java index 13dd1ec75b1..7677f7bb338 100644 --- a/runelite-api/src/main/java/net/runelite/api/WorldEntity.java +++ b/runelite-api/src/main/java/net/runelite/api/WorldEntity.java @@ -33,12 +33,39 @@ public interface WorldEntity extends CameraFocusableEntity /** * Get the location of this world entity in the top level world. + * * @return */ LocalPoint getLocalLocation(); + /** + * Get the orientation of this world entity in the top level world. + * + * @return + */ + int getOrientation(); + + /** + * Get the destination that the WorldEntity is moving toward. + * After receiving a destination from the server, the client will + * interpolate movement along this route until the next game tick + * (with some added buffer for lag compensation). + * + * @return The target {@link LocalPoint} in the top-level {@link WorldView}. + */ + LocalPoint getTargetLocation(); + + /** + * Get the target orientation of this world entity in the top level world. + * + * @return + * @see #getTargetLocation() + */ + int getTargetOrientation(); + /** * Transform a point within the world entity to the overworld + * * @param point * @return */ @@ -46,6 +73,7 @@ public interface WorldEntity extends CameraFocusableEntity /** * Return true if this worldentity is overlapped + * * @return */ boolean isHiddenForOverlap(); diff --git a/runelite-api/src/main/java/net/runelite/api/WorldEntityConfig.java b/runelite-api/src/main/java/net/runelite/api/WorldEntityConfig.java index 3449e928c1c..560cda5e726 100644 --- a/runelite-api/src/main/java/net/runelite/api/WorldEntityConfig.java +++ b/runelite-api/src/main/java/net/runelite/api/WorldEntityConfig.java @@ -29,4 +29,12 @@ public interface WorldEntityConfig int getId(); int getCategory(); + + int getBoundsX(); + + int getBoundsY(); + + int getBoundsWidth(); + + int getBoundsHeight(); } diff --git a/runelite-api/src/main/java/net/runelite/api/WorldView.java b/runelite-api/src/main/java/net/runelite/api/WorldView.java index 72768006371..01bf39cb8fe 100644 --- a/runelite-api/src/main/java/net/runelite/api/WorldView.java +++ b/runelite-api/src/main/java/net/runelite/api/WorldView.java @@ -27,6 +27,7 @@ import javax.annotation.Nullable; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; +import org.intellij.lang.annotations.MagicConstant; public interface WorldView { @@ -255,4 +256,12 @@ Projectile createProjectile(int id, int plane, int startX, int startY, int start */ @Nullable Projection getCanvasProjection(); + + /** + * Returns how clicking on tiles should behave for this WorldView. + * + * @return one of {@link Constants#CLICK_ACTION_NONE}, {@link Constants#CLICK_ACTION_WALK}, {@link Constants#CLICK_ACTION_SET_HEADING} + */ + @MagicConstant(intValues = {Constants.CLICK_ACTION_NONE, Constants.CLICK_ACTION_WALK, Constants.CLICK_ACTION_SET_HEADING}) + int getYellowClickAction(); } diff --git a/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java b/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java index 4208ae80955..c2dae84e95a 100644 --- a/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java +++ b/runelite-api/src/main/java/net/runelite/api/hooks/DrawCallbacks.java @@ -64,6 +64,10 @@ public interface DrawCallbacks * Enable the {@link #zoneInFrustum(int, int, int, int)} callback */ int ZBUF_ZONE_FRUSTUM_CHECK = 0x20; + /** + * Enable the {@link Model#getUnlitFaceColors()} method + */ + int UNLIT_FACE_COLORS = 0x40; int PASS_OPAQUE = 0; int PASS_ALPHA = 1; diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index 9288e065638..f51497766a6 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -41,7 +41,7 @@ nogit false false - 2.0.54 + 2.0.56 nogit diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java index 55d7c5ae505..9e306bcc75e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BarsOres.java @@ -43,6 +43,8 @@ enum BarsOres RUNITE_ORE(VarbitID.BLAST_FURNACE_RUNITE_ORE, ItemID.RUNITE_ORE), SILVER_ORE(VarbitID.BLAST_FURNACE_SILVER_ORE, ItemID.SILVER_ORE), GOLD_ORE(VarbitID.BLAST_FURNACE_GOLD_ORE, ItemID.GOLD_ORE), + LEAD_ORE(VarbitID.BLAST_FURNACE_LEAD_ORE, ItemID.LEAD_ORE), + NICKEL_ORE(VarbitID.BLAST_FURNACE_NICKEL_ORE, ItemID.NICKEL_ORE), BRONZE_BAR(VarbitID.BLAST_FURNACE_BRONZE_BARS, ItemID.BRONZE_BAR), IRON_BAR(VarbitID.BLAST_FURNACE_IRON_BARS, ItemID.IRON_BAR), STEEL_BAR(VarbitID.BLAST_FURNACE_STEEL_BARS, ItemID.STEEL_BAR), @@ -50,7 +52,9 @@ enum BarsOres ADAMANTITE_BAR(VarbitID.BLAST_FURNACE_ADAMANTITE_BARS, ItemID.ADAMANTITE_BAR), RUNITE_BAR(VarbitID.BLAST_FURNACE_RUNITE_BARS, ItemID.RUNITE_BAR), SILVER_BAR(VarbitID.BLAST_FURNACE_SILVER_BARS, ItemID.SILVER_BAR), - GOLD_BAR(VarbitID.BLAST_FURNACE_GOLD_BARS, ItemID.GOLD_BAR); + GOLD_BAR(VarbitID.BLAST_FURNACE_GOLD_BARS, ItemID.GOLD_BAR), + LEAD_BAR(VarbitID.BLAST_FURNACE_LEAD_BARS, ItemID.LEAD_BAR), + CUPRONICKEL_BAR(VarbitID.BLAST_FURNACE_CUPRONICKEL_BARS, ItemID.CUPRONICKEL_BAR); @Getter(onMethod_ = {@Varbit}) private final int varbit; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java index 8d2f6128568..11a4fdd819a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/cluescrolls/clues/CrypticClue.java @@ -241,7 +241,7 @@ public class CrypticClue extends ClueScroll implements NpcClueScroll, ObjectClue .build(), CrypticClue.builder() .itemId(ItemID.TRAIL_CLUE_EASY_VAGUE020) - .text("Search the crate near a cart in Port Khazard.") + .text("Search the crate near the southern general store in Port Khazard.") .location(new WorldPoint(2660, 3149, 0)) .objectId(ObjectID.CRATE) .solution("Search by the southern Khazard General Store in Port Khazard.") diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderConfig.java index 6f72cbac883..1366044d37b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/entityhider/EntityHiderConfig.java @@ -48,8 +48,8 @@ default boolean hideOthers() @ConfigItem( position = 2, keyName = "hidePlayers2D", - name = "Hide others 2D", - description = "Configures whether or not other players 2D elements are hidden." + name = "Hide others' 2D", + description = "Configures whether or not other players' 2D elements are hidden." ) default boolean hideOthers2D() { @@ -169,8 +169,8 @@ default boolean hideWorldEntities() @ConfigItem( position = 20, keyName = "hidePets", - name = "Hide other players' pets", - description = "Configures whether or not other player pets are hidden." + name = "Hide others' pets", + description = "Configures whether or not other players' pets are hidden." ) default boolean hidePets() { @@ -224,8 +224,8 @@ default boolean hideThralls() @ConfigItem( position = 25, keyName = "hideRandomEvents", - name = "Hide random events", - description = "Configures whether or not random events are hidden." + name = "Hide others' random events", + description = "Configures whether or not other players' random events are hidden." ) default boolean hideRandomEvents() { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/FacePrioritySorter.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/FacePrioritySorter.java index 628caa8fb53..d14f007150f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/FacePrioritySorter.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/FacePrioritySorter.java @@ -26,24 +26,18 @@ import java.nio.IntBuffer; import java.util.Arrays; -import javax.inject.Inject; -import javax.inject.Singleton; -import lombok.RequiredArgsConstructor; -import net.runelite.api.Client; import net.runelite.api.Model; import net.runelite.api.Perspective; import net.runelite.api.Projection; -@Singleton -@RequiredArgsConstructor(onConstructor = @__(@Inject)) class FacePrioritySorter { static final int[] distances; static final char[] distanceFaceCount; static final char[][] distanceToFaces; - private static final float[] modelCanvasX; - private static final float[] modelCanvasY; + private static final float[] modelProjectedX; + private static final float[] modelProjectedY; static final float[] modelLocalX; static final float[] modelLocalY; @@ -66,8 +60,8 @@ class FacePrioritySorter distanceFaceCount = new char[MAX_DIAMETER]; distanceToFaces = new char[MAX_DIAMETER][ZSORT_GROUP_SIZE]; - modelCanvasX = new float[MAX_VERTEX_COUNT]; - modelCanvasY = new float[MAX_VERTEX_COUNT]; + modelProjectedX = new float[MAX_VERTEX_COUNT]; + modelProjectedY = new float[MAX_VERTEX_COUNT]; modelLocalX = new float[MAX_VERTEX_COUNT]; modelLocalY = new float[MAX_VERTEX_COUNT]; @@ -80,7 +74,12 @@ class FacePrioritySorter orderedFaces = new int[12][MAX_FACES_PER_PRIORITY]; } - private final Client client; + private final SceneUploader sceneUploader; + + FacePrioritySorter(SceneUploader sceneUploader) + { + this.sceneUploader = sceneUploader; + } int uploadSortedModel(Projection proj, Model model, int orientation, int x, int y, int z, IntBuffer opaqueBuffer, IntBuffer alphaBuffer) { @@ -97,10 +96,6 @@ int uploadSortedModel(Projection proj, Model model, int orientation, int x, int final int[] faceColors3 = model.getFaceColors3(); final byte[] faceRenderPriorities = model.getFaceRenderPriorities(); - final int centerX = client.getCenterX(); - final int centerY = client.getCenterY(); - final int zoom = client.get3dZoom(); - float orientSine = 0; float orientCosine = 0; if (orientation != 0) @@ -140,8 +135,8 @@ int uploadSortedModel(Projection proj, Model model, int orientation, int x, int return 0; } - modelCanvasX[v] = centerX + p[0] * zoom / p[2]; - modelCanvasY[v] = centerY + p[1] * zoom / p[2]; + modelProjectedX[v] = p[0] / p[2]; + modelProjectedY[v] = p[1] / p[2]; distances[v] = (int) p[2] - zero; } @@ -163,12 +158,12 @@ int uploadSortedModel(Projection proj, Model model, int orientation, int x, int final int v3 = indices3[i]; final float - aX = modelCanvasX[v1], - aY = modelCanvasY[v1], - bX = modelCanvasX[v2], - bY = modelCanvasY[v2], - cX = modelCanvasX[v3], - cY = modelCanvasY[v3]; + aX = modelProjectedX[v1], + aY = modelProjectedY[v1], + bX = modelProjectedX[v2], + bY = modelProjectedY[v2], + cX = modelProjectedX[v3], + cY = modelProjectedY[v3]; if ((aX - bX) * (cY - bY) - (cX - bX) * (aY - bY) > 0) { @@ -438,16 +433,16 @@ private int pushFace(Model model, int face, IntBuffer opaqueBuffer, IntBuffer al float vy3 = modelLocalY[triangleC]; float vz3 = modelLocalZ[triangleC]; - SceneUploader.computeFaceUvs(model, face); + sceneUploader.computeFaceUvs(model, face); - int su0 = (int) (SceneUploader.u0 * 256f); - int sv0 = (int) (SceneUploader.v0 * 256f); + int su0 = (int) (sceneUploader.u0 * 256f); + int sv0 = (int) (sceneUploader.v0 * 256f); - int su1 = (int) (SceneUploader.u1 * 256f); - int sv1 = (int) (SceneUploader.v1 * 256f); + int su1 = (int) (sceneUploader.u1 * 256f); + int sv1 = (int) (sceneUploader.v1 * 256f); - int su2 = (int) (SceneUploader.u2 * 256f); - int sv2 = (int) (SceneUploader.v2 * 256f); + int su2 = (int) (sceneUploader.u2 * 256f); + int sv2 = (int) (sceneUploader.v2 * 256f); int alphaBias = 0; alphaBias |= transparencies != null ? (transparencies[face] & 0xff) << 24 : 0; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java index 07d69e84212..b22c12f0efd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/GpuPlugin.java @@ -77,7 +77,6 @@ @PluginDescriptor( name = "GPU", description = "Offloads rendering to GPU", - enabledByDefault = true, tags = {"fog", "draw distance"}, loadInSafeMode = false ) @@ -109,9 +108,6 @@ public class GpuPlugin extends Plugin implements DrawCallbacks @Inject private RegionManager regionManager; - @Inject - private FacePrioritySorter facePrioritySorter; - @Inject private DrawManager drawManager; @@ -173,6 +169,7 @@ public class GpuPlugin extends Plugin implements DrawCallbacks private VAOList vaoPO; private SceneUploader clientUploader, mapUploader; + private FacePrioritySorter facePrioritySorter; static class SceneContext { @@ -261,6 +258,7 @@ protected void startUp() subs = new SceneContext[MAX_WORLDVIEWS]; clientUploader = new SceneUploader(renderCallbackManager); mapUploader = new SceneUploader(renderCallbackManager); + facePrioritySorter = new FacePrioritySorter(clientUploader); clientThread.invoke(() -> { try @@ -1098,26 +1096,29 @@ public void drawPass(Projection projection, Scene scene, int pass) { glUniform3i(uniBase, 0, 0, 0); - var vaos = vaoO.unmap(); - for (VAO vao : vaos) + int sz = vaoO.unmap(); + for (int i = 0; i < sz; ++i) { + VAO vao = vaoO.vaos.get(i); vao.draw(); vao.reset(); } - vaos = vaoPO.unmap(); - if (!vaos.isEmpty()) + sz = vaoPO.unmap(); + if (sz > 0) { glDepthMask(false); - for (VAO vao : vaos) + for (int i = 0; i < sz; ++i) { + VAO vao = vaoPO.vaos.get(i); vao.draw(); } glDepthMask(true); glColorMask(false, false, false, false); - for (VAO vao : vaos) + for (int i = 0; i < sz; ++i) { + VAO vao = vaoPO.vaos.get(i); vao.draw(); vao.reset(); } @@ -1158,7 +1159,7 @@ public void drawDynamic(Projection worldProjection, Scene scene, TileObject tile if (m.getFaceTransparencies() == null) { VAO o = vaoO.get(size); - SceneUploader.uploadTempModel(m, orient, x, y, z, o.vbo.vb); + clientUploader.uploadTempModel(m, orient, x, y, z, o.vbo.vb); } else { @@ -1239,7 +1240,7 @@ public void drawTemp(Projection worldProjection, Scene scene, GameObject gameObj else { VAO o = vaoO.get(size); - SceneUploader.uploadTempModel(m, orient, x, y, z, o.vbo.vb); + clientUploader.uploadTempModel(m, orient, x, y, z, o.vbo.vb); } } @@ -1607,7 +1608,6 @@ public void loadScene(WorldView worldView, Scene scene) Zone[][] newZones = new Zone[SCENE_ZONES][SCENE_ZONES]; final GameState gameState = client.getGameState(); if (prev.isInstance() == scene.isInstance() - && prev.getRoofRemovalMode() == scene.getRoofRemovalMode() && gameState == GameState.LOGGED_IN) { int[][][] prevTemplates = prev.getInstanceTemplateChunks(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java index a32aadb89b2..65c90c91eda 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/SceneUploader.java @@ -698,7 +698,7 @@ else if (color3 == -2) } // temp draw - static int uploadTempModel(Model model, int orientation, int x, int y, int z, IntBuffer opaqueBuffer) + int uploadTempModel(Model model, int orientation, int x, int y, int z, IntBuffer opaqueBuffer) { final int triangleCount = model.getFaceCount(); final int vertexCount = model.getVerticesCount(); @@ -865,9 +865,9 @@ static int interpolateHSL(int hsl, byte hue2, byte sat2, byte lum2, byte lerp) return (hue << 10 | sat << 7 | lum) & 65535; } - static float u0, u1, u2, v0, v1, v2; + float u0, u1, u2, v0, v1, v2; - static void computeFaceUvs(Model model, int face) + void computeFaceUvs(Model model, int face) { final float[] vertexX = model.getVerticesX(); final float[] vertexY = model.getVerticesY(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/VAO.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/VAO.java index e9a1e9d048e..60749bad0c0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/VAO.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/VAO.java @@ -143,7 +143,7 @@ class VAOList private static final int VAO_SIZE = 4 * 1024 * 1024; private int curIdx; - private final List vaos = new ArrayList<>(); + final List vaos = new ArrayList<>(); VAO get(int size) { @@ -174,11 +174,12 @@ VAO get(int size) return vao; } - List unmap() + int unmap() { int sz = 0; - for (VAO vao : vaos) + for (int i = 0; i < vaos.size(); ++i) // NOPMD: ForLoopCanBeForeach { + VAO vao = vaos.get(i); if (vao.vbo.mapped) { ++sz; @@ -186,7 +187,7 @@ List unmap() } } curIdx = 0; - return vaos.subList(0, sz); + return sz; } void free() diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/Zone.java b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/Zone.java index ea2c7a46d54..326212b1524 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/gpu/Zone.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/gpu/Zone.java @@ -522,10 +522,14 @@ void renderAlpha(int zx, int zz, int cyaw, int cpitch, int minLevel, int current int yawcos = Perspective.COSINE[cyaw]; int pitchsin = Perspective.SINE[cpitch]; int pitchcos = Perspective.COSINE[cpitch]; - for (AlphaModel m : alphaModels) + for (int j = 0; j < alphaModels.size(); ++j) // NOPMD: ForLoopCanBeForeach { - if ((m.flags & AlphaModel.SKIP) != 0) continue; - if (m.level != level) continue; + AlphaModel m = alphaModels.get(j); + + if ((m.flags & AlphaModel.SKIP) != 0 || m.level != level) + { + continue; + } boolean ok = false; if (level >= minLevel && level <= maxLevel) @@ -718,8 +722,9 @@ else if (lastDrawMode == STATIC_UNSORTED) void multizoneLocs(Scene scene, int zx, int zz, int cx, int cz, Zone[][] zones) { int offset = scene.getWorldViewId() == -1 ? GpuPlugin.SCENE_OFFSET >> 3 : 0; - for (AlphaModel m : alphaModels) + for (int i = 0; i < alphaModels.size(); ++i) // NOPMD: ForLoopCanBeForeach { + AlphaModel m = alphaModels.get(i); if (m.lx == -1) { continue; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java index 994175b92a5..5e7115e3f2d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/idlenotifier/IdleNotifierPlugin.java @@ -417,6 +417,9 @@ public void onAnimationChanged(AnimationChanged event) case AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_IDLE01: case AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_IDLE01: case AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_INTERACT01: // Sort-salvage + case AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_1X3_RESET01: + case AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_2X5_RESET01: + case AnimationID.SAILING_HUMAN_SALVAGE_HOOK_KANDARIN_3X8_RESET01: /* Misc */ case AnimationID.PISC_REPAIR_HAMMER: case AnimationID.POH_CREATE_MAGIC_TABLET: diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/implings/ImplingsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/implings/ImplingsOverlay.java index 67472b2e49a..585a85f1e14 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/implings/ImplingsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/implings/ImplingsOverlay.java @@ -41,6 +41,8 @@ class ImplingsOverlay extends Overlay { + private static final int PURO_PURO = (40 << 8) | 67; + private final Client client; private final ImplingsConfig config; private final ImplingsPlugin plugin; @@ -58,6 +60,11 @@ private ImplingsOverlay(Client client, ImplingsConfig config, ImplingsPlugin plu @Override public Dimension render(Graphics2D graphics) { + if (client.getLocalPlayer().getWorldLocation().getRegionID() != PURO_PURO) + { + return null; + } + if (config.showSpawn()) { for (ImplingSpawn spawn : ImplingSpawn.values()) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/special/SpicyStew.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/special/SpicyStew.java index de7bf222188..2c0f8e551b0 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/special/SpicyStew.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/special/SpicyStew.java @@ -70,6 +70,7 @@ public StatsChanges calculate(Client client) changes.add(statChangeOf(Stats.THIEVING, yellowBoost, client)); changes.add(statChangeOf(Stats.SLAYER, yellowBoost, client)); changes.add(statChangeOf(Stats.HUNTER, yellowBoost, client)); + changes.add(statChangeOf(Stats.SAILING, yellowBoost, client)); } /* diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/stats/Stats.java b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/stats/Stats.java index 8398ebff94e..666942013a7 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/stats/Stats.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/itemstats/stats/Stats.java @@ -51,5 +51,6 @@ public class Stats public static final Stat RUNECRAFT = new SkillStat(Skill.RUNECRAFT); public static final Stat HUNTER = new SkillStat(Skill.HUNTER); public static final Stat CONSTRUCTION = new SkillStat(Skill.CONSTRUCTION); + public static final Stat SAILING = new SkillStat(Skill.SAILING); public static final Stat RUN_ENERGY = new EnergyStat(); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java index b9866527e9c..6c60cf25321 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/loottracker/LootTrackerPlugin.java @@ -169,6 +169,9 @@ public class LootTrackerPlugin extends Plugin static final String ZOMBIE_PIRATE_LOCKER_EVENT = "Zombie Pirate's Locker"; private static final Pattern ZOMBIE_PIRATE_LOCKER_PATTERN = Pattern.compile("You loot the locker and receive (?[\\d,]+) x (?.+)\\."); + // Shipwreck salvaging + private static final Pattern SALVAGE_PATTERN = Pattern.compile("You sort through the\\s+(?\\S+)\\s+salvage.*"); + // Seed Pack loot handling private static final String SEEDPACK_EVENT = "Seed pack"; @@ -208,6 +211,12 @@ public class LootTrackerPlugin extends Plugin put(5422, "Chest (Aldarin Villas)"). put(6550, "Chest (Moon key)"). put(5521, "Chest (Alchemist's signet)"). + put(12073, "Rusty chest"). + put(7470, "Rusty chest"). + put(6187, "Tarnished chest"). + put(6953, "Tarnished chest"). + put(7743, "Reinforced chest"). + put(8758, "Reinforced chest"). build(); // Chests opened with keys from slayer tasks @@ -1030,6 +1039,15 @@ public void onChatMessage(ChatMessage event) processZombiePirateLockerLoot(zombiePirateLockerMatcher); } + final Matcher shipwreckSalvagingMatcher = SALVAGE_PATTERN.matcher(message); + if (shipwreckSalvagingMatcher.matches()) + { + String tier = shipwreckSalvagingMatcher.group("tier"); + String eventName = WordUtils.capitalizeFully(tier) + " salvage"; + onInvChange(collectInvItems(LootRecordType.EVENT, eventName)); + return; + } + if (message.equals(HERBIBOAR_LOOTED_MESSAGE)) { if (processHerbiboarHerbSackLoot(event.getTimestamp())) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java new file mode 100644 index 00000000000..0d7f4986897 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java @@ -0,0 +1,262 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import net.runelite.client.config.*; +import net.runelite.client.plugins.microbot.util.world.RegionPreference; +import net.runelite.client.plugins.microbot.util.world.WorldSelectionMode; + +@ConfigGroup(BreakHandlerV2Config.configGroup) +public interface BreakHandlerV2Config extends Config { + String configGroup = "break-handler-v2"; + + // ========== BREAK TIMING SECTION ========== + @ConfigSection( + name = "Break Timing", + description = "Configure break timing and duration", + position = 0 + ) + String breakTimingSettings = "breakTimingSettings"; + + @ConfigItem( + keyName = "minPlaytime", + name = "Min Playtime (minutes)", + description = "Minimum time to play before taking a break", + position = 0, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minPlaytime() { + return 45; + } + + @ConfigItem( + keyName = "maxPlaytime", + name = "Max Playtime (minutes)", + description = "Maximum time to play before taking a break", + position = 1, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxPlaytime() { + return 90; + } + + @ConfigItem( + keyName = "minBreakDuration", + name = "Min Break Duration (minutes)", + description = "Minimum break duration", + position = 2, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minBreakDuration() { + return 5; + } + + @ConfigItem( + keyName = "maxBreakDuration", + name = "Max Break Duration (minutes)", + description = "Maximum break duration", + position = 3, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxBreakDuration() { + return 15; + } + + // ========== BREAK BEHAVIOR SECTION ========== + @ConfigSection( + name = "Break Behavior", + description = "Configure how breaks work", + position = 1 + ) + String breakBehaviorOptions = "breakBehaviorOptions"; + + @ConfigItem( + keyName = "logoutOnBreak", + name = "Logout on Break", + description = "Logout when taking a break", + position = 0, + section = breakBehaviorOptions + ) + default boolean logoutOnBreak() { + return true; + } + + @ConfigItem( + keyName = "safetyCheck", + name = "Safety Check", + description = "Wait until not in combat/interaction before breaking (tries up to 12 times over ~60 seconds)", + position = 1, + section = breakBehaviorOptions + ) + default boolean safetyCheck() { + return true; + } + + // ========== LOGIN & WORLD SECTION ========== + @ConfigSection( + name = "Login & World Selection", + description = "Configure login and world selection behavior", + position = 2 + ) + String loginWorldSettings = "loginWorldSettings"; + + @ConfigItem( + keyName = "autoLogin", + name = "Auto Login", + description = "Automatically log back in after break using profile data", + position = 0, + section = loginWorldSettings + ) + default boolean autoLogin() { + return true; + } + + @ConfigItem( + keyName = "worldSelectionMode", + name = "World Selection Mode", + description = "How to select worlds when logging back in", + position = 1, + section = loginWorldSettings + ) + default WorldSelectionMode worldSelectionMode() { + return WorldSelectionMode.CURRENT_PREFERRED_WORLD; + } + + @ConfigItem( + keyName = "regionPreference", + name = "Region Preference", + description = "Preferred region for world selection", + position = 2, + section = loginWorldSettings + ) + default RegionPreference regionPreference() { + return RegionPreference.ANY_REGION; + } + + @ConfigItem( + keyName = "avoidEmptyWorlds", + name = "Avoid Empty Worlds", + description = "Avoid worlds with very few players", + position = 3, + section = loginWorldSettings + ) + default boolean avoidEmptyWorlds() { + return true; + } + + @ConfigItem( + keyName = "avoidOvercrowdedWorlds", + name = "Avoid Crowded Worlds", + description = "Avoid worlds with too many players", + position = 4, + section = loginWorldSettings + ) + default boolean avoidOvercrowdedWorlds() { + return true; + } + + // ========== PROFILE SETTINGS SECTION ========== + @ConfigSection( + name = "Profile Settings", + description = "Profile and account management", + position = 3 + ) + String profileSettings = "profileSettings"; + + @ConfigItem( + keyName = "useActiveProfile", + name = "Use Active Profile", + description = "Use the currently active profile for login credentials", + position = 0, + section = profileSettings + ) + default boolean useActiveProfile() { + return true; + } + + @ConfigItem( + keyName = "respectMemberStatus", + name = "Respect Member Status", + description = "Use profile's member status to select appropriate worlds", + position = 1, + section = profileSettings + ) + default boolean respectMemberStatus() { + return true; + } + + // ========== NOTIFICATIONS SECTION ========== + @ConfigSection( + name = "Notifications", + description = "Discord webhook and notification settings", + position = 4 + ) + String notificationSettings = "notificationSettings"; + + @ConfigItem( + keyName = "enableDiscordWebhook", + name = "Enable Discord Webhook", + description = "Send notifications via Discord webhook from profile", + position = 0, + section = notificationSettings + ) + default boolean enableDiscordWebhook() { + return false; + } + + @ConfigItem( + keyName = "notifyOnBreakStart", + name = "Notify on Break Start", + description = "Send notification when break starts", + position = 1, + section = notificationSettings + ) + default boolean notifyOnBreakStart() { + return true; + } + + @ConfigItem( + keyName = "notifyOnBreakEnd", + name = "Notify on Break End", + description = "Send notification when break ends", + position = 2, + section = notificationSettings + ) + default boolean notifyOnBreakEnd() { + return true; + } + + @ConfigItem( + keyName = "notifyOnLoginFail", + name = "Notify on Login Failure", + description = "Send notification when login fails", + position = 3, + section = notificationSettings + ) + default boolean notifyOnLoginFail() { + return true; + } + + // ========== OVERLAY SETTINGS ========== + @ConfigItem( + keyName = "hideOverlay", + name = "Hide Overlay", + description = "Hide the break handler overlay", + position = 0 + ) + default boolean hideOverlay() { + return false; + } + + @ConfigItem( + keyName = "showDetailedInfo", + name = "Show Detailed Info", + description = "Show detailed information in overlay", + position = 1 + ) + default boolean showDetailedInfo() { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java new file mode 100644 index 00000000000..5b3e3840751 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java @@ -0,0 +1,212 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.ConfigProfile; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.security.LoginManager; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; + +import javax.inject.Inject; +import java.awt.*; +import java.time.Duration; + +/** + * Overlay for Break Handler V2 + * Displays break status, timers, and profile information + */ +@Slf4j +public class BreakHandlerV2Overlay extends OverlayPanel { + + private final BreakHandlerV2Config config; + private final BreakHandlerV2Script script; + + @Inject + public BreakHandlerV2Overlay(BreakHandlerV2Config config, BreakHandlerV2Script script) { + super(); + this.config = config; + this.script = script; + setPosition(OverlayPosition.TOP_LEFT); + panelComponent.setPreferredSize(new Dimension(300, 300)); + } + + @Override + public Dimension render(Graphics2D graphics) { + try { + // Check if overlay should be hidden + if (config.hideOverlay()) { + return null; + } + + + panelComponent.getChildren().clear(); + + // Title + panelComponent.getChildren().add(TitleComponent.builder() + .text("Break Handler V2") + .color(Color.CYAN) + .build()); + + // Current state + BreakHandlerV2State currentState = BreakHandlerV2State.getCurrentState(); + Color stateColor = getStateColor(currentState); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Status:") + .right(currentState.getDescription()) + .rightColor(stateColor) + .build()); + + // Version + panelComponent.getChildren().add(LineComponent.builder() + .left("Version:") + .right(BreakHandlerV2Script.version) + .rightColor(Color.GRAY) + .build()); + + // Time until break or break remaining + if (currentState == BreakHandlerV2State.WAITING_FOR_BREAK) { + long secondsUntilBreak = script.getTimeUntilBreak(); + //if (secondsUntilBreak >= 0) { + String timeStr = formatDuration(secondsUntilBreak); + panelComponent.getChildren().add(LineComponent.builder() + .left("Next break:") + .right(timeStr) + .rightColor(Color.GREEN) + .build()); + //} + } else if (BreakHandlerV2State.isBreakActive()) { + long secondsRemaining = script.getBreakTimeRemaining(); + if (secondsRemaining >= 0) { + String timeStr = formatDuration(secondsRemaining); + panelComponent.getChildren().add(LineComponent.builder() + .left("Break ends:") + .right(timeStr) + .rightColor(Color.ORANGE) + .build()); + } + } + + // Show detailed info if enabled + if (config.showDetailedInfo()) { + // Profile information + ConfigProfile profile = LoginManager.getActiveProfile(); + if (profile != null) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Profile:") + .right(profile.getName()) + .rightColor(Color.WHITE) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Member:") + .right(profile.isMember() ? "Yes" : "No") + .rightColor(profile.isMember() ? Color.YELLOW : Color.GRAY) + .build()); + } + + // World selection mode + panelComponent.getChildren().add(LineComponent.builder() + .left("World mode:") + .right(config.worldSelectionMode().name()) + .rightColor(Color.LIGHT_GRAY) + .build()); + + // Region preference + if (config.regionPreference() != null) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Region:") + .right(config.regionPreference().name()) + .rightColor(Color.LIGHT_GRAY) + .build()); + } + + // Current world if logged in + if (Microbot.isLoggedIn()) { + int currentWorld = Microbot.getClient().getWorld(); + panelComponent.getChildren().add(LineComponent.builder() + .left("Current world:") + .right(String.valueOf(currentWorld)) + .rightColor(Color.GREEN) + .build()); + } + + // Break configuration + panelComponent.getChildren().add(LineComponent.builder() + .left("Break type:") + .right(config.logoutOnBreak() ? "Logout" : "Stay logged in") + .rightColor(Color.LIGHT_GRAY) + .build()); + + // Auto-login status + panelComponent.getChildren().add(LineComponent.builder() + .left("Auto-login:") + .right(config.autoLogin() ? "Enabled" : "Disabled") + .rightColor(config.autoLogin() ? Color.GREEN : Color.RED) + .build()); + + // Discord notifications + if (config.enableDiscordWebhook()) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Discord:") + .right("Enabled") + .rightColor(Color.CYAN) + .build()); + } + } + + } catch (Exception ex) { + log.error("[BreakHandlerV2Overlay] Error rendering overlay", ex); + } + + return super.render(graphics); + } + + /** + * Get color for current state + */ + private Color getStateColor(BreakHandlerV2State state) { + switch (state) { + case WAITING_FOR_BREAK: + return Color.GREEN; + case BREAK_REQUESTED: + case INITIATING_BREAK: + return Color.YELLOW; + case LOGOUT_REQUESTED: + case LOGGED_OUT: + return Color.ORANGE; + case LOGIN_REQUESTED: + case LOGGING_IN: + return Color.CYAN; + case LOGIN_EXTENDED_SLEEP: + return Color.RED; + case BREAK_ENDING: + return Color.LIGHT_GRAY; + case PROFILE_SWITCHING: + return Color.MAGENTA; + default: + return Color.WHITE; + } + } + + /** + * Format duration in seconds to human-readable string + */ + private String formatDuration(long seconds) { + Duration duration = Duration.ofSeconds(seconds); + + long hours = duration.toHours(); + long minutes = duration.toMinutesPart(); + long secs = duration.toSecondsPart(); + + if (hours > 0) { + return String.format("%dh %dm %ds", hours, minutes, secs); + } else if (minutes > 0) { + return String.format("%dm %ds", minutes, secs); + } else { + return String.format("%ds", secs); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Plugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Plugin.java new file mode 100644 index 00000000000..0d14ee15370 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Plugin.java @@ -0,0 +1,111 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import com.google.inject.Provides; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.events.GameStateChanged; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; + +import javax.inject.Inject; +import java.awt.*; + +/** + * Break Handler V2 Plugin + * Enhanced break handler with profile-based auto-login and intelligent world selection + * + * Features: + * - Automatic login using profile data (username, password, member status) + * - Intelligent world selection based on multiple modes: + * * Current/Preferred world + * * Random accessible world + * * Regional selection + * * Best population balance + * * Best ping performance + * - Configurable break timing (min/max playtime and break duration) + * - Optional in-game breaks (pause scripts without logout) + * - Safety checks (waits for combat/interaction to end) + * - Discord webhook notifications + * - Profile-aware member/F2P world selection + * - Retry mechanism with configurable attempts and delays + * - In-game overlay showing break status and timers + * + * @version 2.0.0 + */ +@PluginDescriptor( + name = PluginDescriptor.Default + "BreakHandler V2", + description = "Advanced break handler with profile-based login and world selection", + tags = {"break", "microbot", "breakhandler", "login", "world", "profile", "v2"}, + enabledByDefault = false +) +@Slf4j +public class BreakHandlerV2Plugin extends Plugin { + + @Inject + private BreakHandlerV2Config config; + + @Inject + private BreakHandlerV2Script script; + + @Inject + private OverlayManager overlayManager; + + @Inject + private BreakHandlerV2Overlay overlay; + + @Provides + BreakHandlerV2Config provideConfig(ConfigManager configManager) { + return configManager.getConfig(BreakHandlerV2Config.class); + } + + @Override + protected void startUp() throws AWTException { + log.info("[BreakHandlerV2] Plugin starting up"); + System.out.println("[DEBUG] BreakHandlerV2Plugin.startUp() with script instance hash: " + System.identityHashCode(script)); + + // Add in-game overlay + if (overlayManager != null && overlay != null) { + overlayManager.add(overlay); + log.info("[BreakHandlerV2] In-game overlay added"); + } + + // Start the script + if (script != null) { + script.run(config); + log.info("[BreakHandlerV2] Script started"); + } + + log.info("[BreakHandlerV2] Plugin started successfully (v{})", BreakHandlerV2Script.version); + } + + @Override + protected void shutDown() { + log.info("[BreakHandlerV2] Plugin shutting down"); + + // Shutdown script + if (script != null) { + script.shutdown(); + log.info("[BreakHandlerV2] Script stopped"); + } + + // Remove in-game overlay + if (overlayManager != null && overlay != null) { + overlayManager.remove(overlay); + log.info("[BreakHandlerV2] In-game overlay removed"); + } + + log.info("[BreakHandlerV2] Plugin shut down successfully"); + } + + /** + * Handle game state changes + * Can be used to detect unexpected logouts or other state changes + */ + @Subscribe + public void onGameStateChanged(GameStateChanged event) { + // Future implementation: detect unexpected logouts, bans, etc. + log.debug("[BreakHandlerV2] Game state changed: {}", event.getGameState()); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java new file mode 100644 index 00000000000..613d3a618f4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java @@ -0,0 +1,703 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.GameState; +import net.runelite.client.config.ConfigProfile; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.discord.Rs2Discord; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.security.Login; +import net.runelite.client.plugins.microbot.util.security.LoginManager; +import net.runelite.client.plugins.microbot.util.world.Rs2WorldUtil; +import net.runelite.client.ui.ClientUI; +import net.runelite.http.api.worlds.WorldRegion; + +import javax.inject.Singleton; +import java.awt.Color; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +/** + * Break Handler V2 Script + * Enhanced break handler with profile-based login and intelligent world selection + * Version: 2.0.0 + */ +@Singleton +@Slf4j +public class BreakHandlerV2Script extends Script { + + // Instance tracking for debugging + private static int instanceCounter = 0; + private final int instanceId; + + public BreakHandlerV2Script() { + instanceId = ++instanceCounter; + System.out.println("[DEBUG] BreakHandlerV2Script instance #" + instanceId + " created. Hash: " + System.identityHashCode(this)); + } + + @Getter + private BreakHandlerV2Config config; + + // Timing variables (volatile for thread visibility from overlay/UI threads) + private volatile Instant nextBreakTime; + private volatile Instant breakEndTime; + private volatile Instant loginAttemptTime; + + // State tracking + private int loginRetryCount = 0; + private int safetyCheckAttempts = 0; + private int preBreakWorld = -1; + private ConfigProfile activeProfile; + private boolean unexpectedLogoutDetected = false; + private String originalWindowTitle = ""; + + // Break duration in milliseconds + private long currentBreakDuration = 0; + + // Login retry backoff constants + private static final int MAX_LOGIN_ATTEMPTS = 10; + private static final int INITIAL_FAST_RETRIES = 3; + private static final int BACKOFF_BASE_DELAY_MS = 30000; // 30 seconds + + // Safety check backoff constants + private static final int MAX_SAFETY_CHECK_ATTEMPTS = 60; + private static final int SAFETY_CHECK_DELAY_MS = 5000; // 5 seconds between checks + + public static String version = "2.0.0"; + + /** + * Run the break handler script + */ + public boolean run(BreakHandlerV2Config config) { + this.config = config; + BreakHandlerV2State.setState(BreakHandlerV2State.WAITING_FOR_BREAK); + + // Initialize next break time immediately to prevent null values in overlay + scheduleNextBreak(); + log.info("[BreakHandlerV2] Initial break scheduled for {}", nextBreakTime); + // Load active profile + loadActiveProfile(); + originalWindowTitle = ClientUI.getFrame().getTitle(); + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!super.run()) return; + + + + // Detect unexpected logout while waiting for break + detectUnexpectedLogout(); + updateWindowTitle(); + + // Main state machine + switch (BreakHandlerV2State.getCurrentState()) { + case WAITING_FOR_BREAK: + handleWaitingForBreak(); + break; + case BREAK_REQUESTED: + handleBreakRequested(); + break; + case INITIATING_BREAK: + handleInitiatingBreak(); + break; + case LOGOUT_REQUESTED: + handleLogoutRequested(); + break; + case LOGGED_OUT: + handleLoggedOut(); + break; + case LOGIN_REQUESTED: + handleLoginRequested(); + break; + case LOGGING_IN: + handleLoggingIn(); + break; + case LOGIN_EXTENDED_SLEEP: + handleLoginExtendedSleep(); + break; + case BREAK_ENDING: + handleBreakEnding(); + break; + case PROFILE_SWITCHING: + handleProfileSwitching(); + break; + } + + } catch (Exception ex) { + log.error("[BreakHandlerV2] Error in main loop", ex); + } + }, 0, 1000, TimeUnit.MILLISECONDS); + + return true; + } + + /** + * Load the active profile from config manager + */ + private void loadActiveProfile() { + if (config.useActiveProfile()) { + try { + activeProfile = Microbot.getConfigManager().getProfile(); + if (activeProfile != null) { + LoginManager.setActiveProfile(activeProfile); + } + } catch (Exception ex) { + log.error("[BreakHandlerV2] Failed to load active profile", ex); + } + } + } + + /** + * Handle WAITING_FOR_BREAK state + * Schedules next break and monitors for break time + */ + private void handleWaitingForBreak() { + // Check if it's time for a break + if (nextBreakTime != null && Instant.now().isAfter(nextBreakTime)) { + log.info("[BreakHandlerV2] Break time reached, requesting break"); + transitionToState(BreakHandlerV2State.BREAK_REQUESTED); + } + } + + /** + * Handle BREAK_REQUESTED state + * Initiates break based on configuration + */ + private void handleBreakRequested() { + // If breakEndTime is already set, we're in a no-logout break waiting for it to end + if (breakEndTime != null) { + // Check if break is over + if (Instant.now().isAfter(breakEndTime)) { + log.info("[BreakHandlerV2] No-logout break ended"); + Microbot.pauseAllScripts.set(false); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + } + return; + } + + // Store current world before break + if (Microbot.isLoggedIn()) { + preBreakWorld = Microbot.getClient().getWorld(); + } + + if (config.logoutOnBreak()) { + log.info("[BreakHandlerV2] Starting break (with logout)"); + transitionToState(BreakHandlerV2State.INITIATING_BREAK); + } else { + log.info("[BreakHandlerV2] Starting break (no logout - scripts paused)"); + currentBreakDuration = calculateBreakDuration(); + breakEndTime = Instant.now().plus(currentBreakDuration, ChronoUnit.MILLIS); + + sendDiscordNotification("Break Started", + "Duration: " + (currentBreakDuration / 60000) + " minutes (no logout)"); + + // Pause all scripts and stay in this state until break ends + Microbot.pauseAllScripts.set(true); + } + } + + /** + * Handle INITIATING_BREAK state + * Performs safety checks before logout with backoff retry + */ + private void handleInitiatingBreak() { + if (!Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Already logged out, transitioning to LOGGED_OUT"); + currentBreakDuration = calculateBreakDuration(); + breakEndTime = Instant.now().plus(currentBreakDuration, ChronoUnit.MILLIS); + safetyCheckAttempts = 0; // Reset counter + transitionToState(BreakHandlerV2State.LOGGED_OUT); + return; + } + + // Safety check if enabled + if (config.safetyCheck()) { + boolean isInCombat = Rs2Player.isInCombat(); + boolean isInteracting = Rs2Player.isInteracting(); + + if (isInCombat || isInteracting) { + safetyCheckAttempts++; + + if (safetyCheckAttempts >= MAX_SAFETY_CHECK_ATTEMPTS) { + log.warn("[BreakHandlerV2] Safety check max attempts ({}) reached, forcing break", + MAX_SAFETY_CHECK_ATTEMPTS); + + String unsafeReason = isInCombat && isInteracting ? "in combat and interacting" + : isInCombat ? "in combat" + : "interacting"; + + sendDiscordNotification("Safety Check Failed", + "Failed to achieve safe conditions after " + MAX_SAFETY_CHECK_ATTEMPTS + " attempts.\n" + + "Player still " + unsafeReason + ".\n" + + "Forcing break anyway."); + + safetyCheckAttempts = 0; // Reset counter + } else { + log.debug("[BreakHandlerV2] Waiting for safe conditions... (attempt {}/{})", + safetyCheckAttempts, MAX_SAFETY_CHECK_ATTEMPTS); + sleep(SAFETY_CHECK_DELAY_MS); + return; // Stay in this state and check again + } + } else { + // Safe conditions met + if (safetyCheckAttempts > 0) { + log.info("[BreakHandlerV2] Safe conditions achieved after {} attempts", safetyCheckAttempts); + } + safetyCheckAttempts = 0; // Reset counter + } + } + + // Proceed to logout + currentBreakDuration = calculateBreakDuration(); + breakEndTime = Instant.now().plus(currentBreakDuration, ChronoUnit.MILLIS); + + sendDiscordNotification("Break Started", + "Type: Logout break\nDuration: " + (currentBreakDuration / 60000) + " minutes"); + + transitionToState(BreakHandlerV2State.LOGOUT_REQUESTED); + } + + /** + * Handle LOGOUT_REQUESTED state + * Performs logout + */ + private void handleLogoutRequested() { + if (!Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Logout successful"); + transitionToState(BreakHandlerV2State.LOGGED_OUT); + return; + } + + try { + log.info("[BreakHandlerV2] Attempting logout..."); + Rs2Player.logout(); + sleep(2000, 3000); + } catch (Exception ex) { + log.error("[BreakHandlerV2] Error during logout", ex); + } + } + + /** + * Handle LOGGED_OUT state + * Waits for break duration to complete + */ + private void handleLoggedOut() { + if (breakEndTime == null) { + log.error("[BreakHandlerV2] Break end time not set, resetting"); + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + return; + } + + // Check if break is over + if (Instant.now().isAfter(breakEndTime)) { + if (config.autoLogin()) { + log.info("[BreakHandlerV2] Break ended, requesting login"); + loginRetryCount = 0; + transitionToState(BreakHandlerV2State.LOGIN_REQUESTED); + } else { + log.info("[BreakHandlerV2] Break ended, auto-login disabled"); + sendDiscordNotification("Break Ended", "Auto-login is disabled"); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + } + } + } + + /** + * Handle LOGIN_REQUESTED state + * Initiates login with profile data and world selection + * Uses exponential backoff: first 3 attempts are fast, then 30s incremental delays + */ + private void handleLoginRequested() { + // Check if already logged in + if (Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Already logged in"); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + return; + } + + // Check retry limit (max 10 attempts) + if (loginRetryCount >= MAX_LOGIN_ATTEMPTS) { + log.error("[BreakHandlerV2] Max login attempts ({}) reached", MAX_LOGIN_ATTEMPTS); + sendDiscordNotification("Login Failed", + "Max login attempts (" + MAX_LOGIN_ATTEMPTS + ") reached. Giving up."); + transitionToState(BreakHandlerV2State.LOGIN_EXTENDED_SLEEP); + return; + } + + // Validate profile + if (activeProfile == null) { + log.error("[BreakHandlerV2] No active profile available for login"); + sendDiscordNotification("Login Failed", "No active profile available"); + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + return; + } + + // Apply backoff delay if needed (after first 3 attempts) + if (loginRetryCount >= INITIAL_FAST_RETRIES) { + int backoffDelay = calculateLoginBackoffDelay(loginRetryCount); + log.info("[BreakHandlerV2] Applying backoff delay: {} seconds", backoffDelay / 1000); + sleep(backoffDelay); + } else if (loginRetryCount > 0) { + // Small delay between initial fast retries (5 seconds) + sleep(5000); + } + + // Select world based on configuration + int targetWorld = selectWorld(); + + if (targetWorld == -1) { + log.error("[BreakHandlerV2] Failed to select valid world"); + loginRetryCount++; + return; + } + + log.info("[BreakHandlerV2] Attempting login to world {} (attempt {}/{})", + targetWorld, loginRetryCount + 1, MAX_LOGIN_ATTEMPTS); + + // Perform login + boolean loginInitiated = LoginManager.login( + activeProfile.getName(), + activeProfile.getPassword(), + targetWorld + ); + + if (loginInitiated) { + loginRetryCount++; + loginAttemptTime = Instant.now(); + transitionToState(BreakHandlerV2State.LOGGING_IN); + } else { + log.error("[BreakHandlerV2] Failed to initiate login"); + loginRetryCount++; + } + } + + /** + * Calculate exponential backoff delay for login retries + * First 3 attempts: 5s delay + * After that: 30s, 60s, 90s, 120s, etc. + */ + private int calculateLoginBackoffDelay(int attemptCount) { + if (attemptCount < INITIAL_FAST_RETRIES) { + return 5000; // 5 seconds for initial retries + } + // Exponential backoff: 30s * (attempt - 3) + int backoffMultiplier = attemptCount - INITIAL_FAST_RETRIES + 1; + return BACKOFF_BASE_DELAY_MS * backoffMultiplier; + } + + /** + * Handle LOGGING_IN state + * Monitors login progress + */ + private void handleLoggingIn() { + // Check if logged in + if (Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Login successful"); + sendDiscordNotification("Login Successful", + "Logged into world " + Microbot.getClient().getWorld()); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + return; + } + + // Check for timeout (60 seconds) + if (loginAttemptTime != null && + Instant.now().isAfter(loginAttemptTime.plusSeconds(60))) { + log.warn("[BreakHandlerV2] Login timeout, retrying"); + transitionToState(BreakHandlerV2State.LOGIN_REQUESTED); + } + } + + /** + * Handle LOGIN_EXTENDED_SLEEP state + * Extended wait after multiple failed login attempts + */ + private void handleLoginExtendedSleep() { + log.info("[BreakHandlerV2] Entering extended sleep (5 minutes)"); + sleep(300000); // 5 minutes + loginRetryCount = 0; + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + } + + /** + * Handle BREAK_ENDING state + * Finalizes break and schedules next break + */ + private void handleBreakEnding() { + log.info("[BreakHandlerV2] Break cycle complete"); + + // Reset variables + breakEndTime = null; + loginAttemptTime = null; + loginRetryCount = 0; + safetyCheckAttempts = 0; + preBreakWorld = -1; + unexpectedLogoutDetected = false; + + // Unpause scripts + Microbot.pauseAllScripts.set(false); + + // Schedule next break + scheduleNextBreak(); + + sendDiscordNotification("Break Ended", + "Next break scheduled for " + nextBreakTime); + + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + } + + /** + * Handle PROFILE_SWITCHING state + * Future implementation for multi-account support + */ + private void handleProfileSwitching() { + // Placeholder for future profile switching functionality + log.info("[BreakHandlerV2] Profile switching not yet implemented"); + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + } + + /** + * Detect unexpected logout (kicked, disconnected, etc.) + * Handles case where player is logged out while waiting for a scheduled break + */ + private void detectUnexpectedLogout() { + // Only check if we're in WAITING_FOR_BREAK state and have a scheduled break + if (BreakHandlerV2State.getCurrentState() != BreakHandlerV2State.WAITING_FOR_BREAK) { + unexpectedLogoutDetected = false; // Reset flag when not in WAITING_FOR_BREAK + return; + } + + if (nextBreakTime == null) { + return; + } + + // Reset flag when player is logged in + if (Microbot.isLoggedIn()) { + unexpectedLogoutDetected = false; + return; + } + + // Check if player is logged out unexpectedly + if (!Microbot.isLoggedIn() && !unexpectedLogoutDetected) { + long secondsUntilBreak = Instant.now().until(nextBreakTime, ChronoUnit.SECONDS); + + if (secondsUntilBreak > 0) { + log.warn("[BreakHandlerV2] Unexpected logout detected with {}s until scheduled break", secondsUntilBreak); + unexpectedLogoutDetected = true; // Prevent repeated detections + + // Handle based on configuration + if (config.autoLogin()) { + log.info("[BreakHandlerV2] Auto-login enabled, attempting to log back in"); + loginRetryCount = 0; + transitionToState(BreakHandlerV2State.LOGIN_REQUESTED); + } else { + log.info("[BreakHandlerV2] Auto-login disabled, pausing break timer"); + // Keep the state as WAITING_FOR_BREAK but don't count time while logged out + // The timer will resume when player logs back in manually + sendDiscordNotification("Unexpected Logout", + "Player logged out with " + (secondsUntilBreak / 60) + " minutes until break.\nAuto-login is disabled."); + } + } + } + } + + /** + * Select world based on configuration and profile + */ + private int selectWorld() { + boolean membersOnly = config.respectMemberStatus() && + activeProfile != null && + activeProfile.isMember(); + + WorldRegion region = config.regionPreference().getWorldRegion(); + + int targetWorld = -1; + + switch (config.worldSelectionMode()) { + case CURRENT_PREFERRED_WORLD: + targetWorld = preBreakWorld != -1 ? preBreakWorld : + Rs2WorldUtil.getRandomAccessibleWorldFromRegion( + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case RANDOM_WORLD: + targetWorld = Rs2WorldUtil.getRandomAccessibleWorld( + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case REGIONAL_RANDOM: + targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case BEST_POPULATION: + targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( + false, // by population, not ping + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case BEST_PING: + targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( + true, // by ping + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + } + + log.info("[BreakHandlerV2] Selected world: {} (mode: {}, members: {})", + targetWorld, config.worldSelectionMode(), membersOnly); + + return targetWorld; + } + + /** + * Schedule the next break + */ + private void scheduleNextBreak() { + int minMinutes = config.minPlaytime(); + int maxMinutes = config.maxPlaytime(); + + int playtimeMinutes = Rs2Random.between(minMinutes, maxMinutes); + nextBreakTime = Instant.now().plus(playtimeMinutes, ChronoUnit.MINUTES); + + log.info("[BreakHandlerV2] Next break in {} minutes", playtimeMinutes); + } + + /** + * Calculate break duration + */ + private long calculateBreakDuration() { + int minMinutes = config.minBreakDuration(); + int maxMinutes = config.maxBreakDuration(); + + int breakMinutes = Rs2Random.between(minMinutes, maxMinutes); + log.info("[BreakHandlerV2] Break duration: {} minutes", breakMinutes); + + return breakMinutes * 60000L; // Convert to milliseconds + } + + /** + * Transition to a new state + */ + private void transitionToState(BreakHandlerV2State newState) { + BreakHandlerV2State oldState = BreakHandlerV2State.getCurrentState(); + log.info("[BreakHandlerV2] State transition: {} -> {}", oldState, newState); + BreakHandlerV2State.setState(newState); + } + + /** + * Send Discord notification if enabled + */ + private void sendDiscordNotification(String title, String message) { + if (!config.enableDiscordWebhook()) { + return; + } + + if (activeProfile == null || activeProfile.getDiscordWebhookUrl() == null) { + return; + } + + try { + String playerName = activeProfile.getName(); + Rs2Discord.sendCustomNotification( + title, + message, + Rs2Discord.convertColorToInt(Color.CYAN), + playerName != null ? playerName : "Unknown", + "BreakHandler V2" + ); + } catch (Exception ex) { + log.error("[BreakHandlerV2] Failed to send Discord notification", ex); + } + } + + /** + * Get time until next break in seconds + */ + public long getTimeUntilBreak() { + if (nextBreakTime == null) { + return -1; + } + return Instant.now().until(nextBreakTime, ChronoUnit.SECONDS); + } + + /** + * Get time remaining in break in seconds + */ + public long getBreakTimeRemaining() { + if (breakEndTime == null) { + return -1; + } + return Instant.now().until(breakEndTime, ChronoUnit.SECONDS); + } + + @Override + public void shutdown() { + super.shutdown(); + log.info("[BreakHandlerV2] Shutting down"); + + // Reset state + BreakHandlerV2State.setState(BreakHandlerV2State.WAITING_FOR_BREAK); + Microbot.pauseAllScripts.set(false); + + // Clear timers + nextBreakTime = null; + breakEndTime = null; + loginAttemptTime = null; + + // Reset counters and flags + unexpectedLogoutDetected = false; + loginRetryCount = 0; + safetyCheckAttempts = 0; + } + + private void updateWindowTitle() { + BreakHandlerV2State state = BreakHandlerV2State.getCurrentState(); + + if (getBreakTimeRemaining() > 0) { + ClientUI.getFrame().setTitle(originalWindowTitle + " - " + state.toString() + ": " + + formatDuration(Duration.ofSeconds(Math.max(0, getBreakTimeRemaining())))); + } + } + + /** + * Formats a duration with header text. + */ + public static String formatDuration(Duration duration, String header) { + return String.format(header + " %s", formatDuration(duration)); + } + + /** + * Formats a duration into HH:MM:SS format. + */ + public static String formatDuration(Duration duration) { + if (duration == null || duration.isNegative() || duration.isZero()) { + return "00:00:00"; + } + long hours = duration.toHours(); + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() % 60; + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2State.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2State.java new file mode 100644 index 00000000000..8fb6626d47c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2State.java @@ -0,0 +1,77 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * State machine for Break Handler V2 + * Manages all states and transitions for the break system + */ +@Getter +@RequiredArgsConstructor +public enum BreakHandlerV2State { + WAITING_FOR_BREAK("Waiting for break"), + BREAK_REQUESTED("Break requested"), + INITIATING_BREAK("Initiating break"), + LOGOUT_REQUESTED("Logout requested"), + LOGGED_OUT("Logged out"), + LOGIN_REQUESTED("Login requested"), + LOGGING_IN("Logging in"), + LOGIN_EXTENDED_SLEEP("Login extended sleep"), + BREAK_ENDING("Break ending"), + PROFILE_SWITCHING("Switching profile"); + + private final String description; + + private static final AtomicReference currentState = + new AtomicReference<>(WAITING_FOR_BREAK); + + /** + * Get the current state (thread-safe) + */ + public static BreakHandlerV2State getCurrentState() { + return currentState.get(); + } + + /** + * Set the current state (thread-safe) + */ + public static void setState(BreakHandlerV2State newState) { + currentState.set(newState); + } + + /** + * Check if break is currently active + */ + public static boolean isBreakActive() { + BreakHandlerV2State state = getCurrentState(); + return state == BREAK_REQUESTED || + state == INITIATING_BREAK || + state == LOGOUT_REQUESTED || + state == LOGGED_OUT || + state == LOGIN_REQUESTED || + state == LOGGING_IN || + state == LOGIN_EXTENDED_SLEEP || + state == BREAK_ENDING || + state == PROFILE_SWITCHING; + } + + /** + * Check if this is a lock state that prevents new breaks + */ + public boolean isLockState() { + return this == BREAK_REQUESTED || + this == INITIATING_BREAK || + this == LOGOUT_REQUESTED || + this == LOGGING_IN || + this == BREAK_ENDING || + this == PROFILE_SWITCHING; + } + + @Override + public String toString() { + return description; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java index e15d3d711d5..06e3516c1d3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginClient.java @@ -24,6 +24,7 @@ */ package net.runelite.client.plugins.microbot.externalplugins; +import com.google.common.base.Strings; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; @@ -32,19 +33,33 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; import javax.imageio.ImageIO; import javax.inject.Inject; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; @Slf4j public class MicrobotPluginClient { private static final HttpUrl MICROBOT_PLUGIN_HUB_URL = HttpUrl.parse("https://chsami.github.io/Microbot-Hub/"); + private static final HttpUrl MICROBOT_PLUGIN_REPOSITORY_URL = HttpUrl.parse( + "https://nexus.microbot.cloud/repository/microbot-plugins/net/runelite/client/plugins/microbot/" + ); private static final String PLUGINS_JSON_PATH = "plugins.json"; private final OkHttpClient okHttpClient; @@ -112,8 +127,21 @@ public BufferedImage downloadIcon(String iconUrl) throws IOException /** * Returns the URL for downloading a plugin JAR */ - public HttpUrl getJarURL(MicrobotPluginManifest manifest) + public HttpUrl getJarURL(MicrobotPluginManifest manifest, String versionOverride) { + String artifactId = manifest.getInternalName(); + String version = !Strings.isNullOrEmpty(versionOverride) ? versionOverride : manifest.getVersion(); + if (MICROBOT_PLUGIN_REPOSITORY_URL != null && !Strings.isNullOrEmpty(artifactId) && !Strings.isNullOrEmpty(version)) + { + String artifactPath = sanitizeArtifactId(artifactId); + String fileName = artifactPath + "-" + version + ".jar"; + return MICROBOT_PLUGIN_REPOSITORY_URL.newBuilder() + .addPathSegment(artifactPath) + .addPathSegment(version) + .addPathSegment(fileName) + .build(); + } + return HttpUrl.parse(manifest.getUrl()); } @@ -126,4 +154,76 @@ public Map getPluginCounts() throws IOException // For now, we'll return an empty map return Map.of(); } + + /** + * Fetches the list of published versions for the given plugin from the Microbot Nexus repository. + */ + public List fetchAvailableVersions(String internalName) throws IOException + { + if (internalName == null || internalName.isEmpty() || MICROBOT_PLUGIN_REPOSITORY_URL == null) + { + return Collections.emptyList(); + } + + HttpUrl metadataUrl = MICROBOT_PLUGIN_REPOSITORY_URL.newBuilder() + .addPathSegment(sanitizeArtifactId(internalName)) + .addPathSegment("maven-metadata.xml") + .build(); + + Request request = new Request.Builder() + .url(metadataUrl) + .header("Cache-Control", "no-cache") + .build(); + + try (Response res = okHttpClient.newCall(request).execute()) + { + if (res.body() == null || res.code() != 200) + { + throw new IOException("Failed to fetch metadata for " + internalName + ": HTTP " + res.code()); + } + + String xml = res.body().string(); + return parseVersionList(xml); + } + catch (ParserConfigurationException | SAXException ex) + { + throw new IOException("Unable to parse metadata for " + internalName, ex); + } + } + + private List parseVersionList(String xml) throws ParserConfigurationException, IOException, SAXException + { + if (xml == null || xml.isEmpty()) + { + return Collections.emptyList(); + } + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(xml))); + NodeList versionNodes = document.getElementsByTagName("version"); + List versions = new ArrayList<>(versionNodes.getLength()); + + for (int i = 0; i < versionNodes.getLength(); i++) + { + String text = versionNodes.item(i).getTextContent(); + if (text != null && !text.isBlank()) + { + versions.add(text.trim()); + } + } + + return versions; + } + + private String sanitizeArtifactId(String artifactId) + { + return artifactId == null ? "" : artifactId.toLowerCase(Locale.ROOT); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java index a3d695cbb74..d30a6b8e4e4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManager.java @@ -25,6 +25,7 @@ package net.runelite.client.plugins.microbot.externalplugins; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import com.google.common.graph.Graph; import com.google.common.graph.GraphBuilder; import com.google.common.graph.Graphs; @@ -82,6 +83,8 @@ @Singleton public class MicrobotPluginManager { private static final File PLUGIN_DIR = new File(RuneLite.RUNELITE_DIR, "microbot-plugins"); + private static final String INSTALLED_VERSION_GROUP = "microbotPluginVersions"; + private static final String INSTALLED_VERSION_KEY_PREFIX = "plugin."; private final OkHttpClient okHttpClient; private final MicrobotPluginClient microbotPluginClient; @@ -141,6 +144,13 @@ private void loadManifest() { Map next = new HashMap<>(manifests.size()); for (MicrobotPluginManifest m : manifests) { next.put(m.getInternalName(), m); + try { + List versions = microbotPluginClient.fetchAvailableVersions(m.getInternalName()); + m.setAvailableVersions(versions); + } catch (IOException ex) { + log.warn("Failed to fetch available versions for {}: {}", m.getInternalName(), ex.getMessage()); + log.debug("Version fetch error", ex); + } } boolean changed = !next.keySet().equals(manifestMap.keySet()) || next.entrySet().stream().anyMatch(e -> { @@ -214,14 +224,13 @@ private boolean verifyHash(String internalName) { } MicrobotPluginManifest authoritativeManifest = manifestMap.get(internalName); + InstalledPluginVersion storedVersion = lookupInstalledPluginVersion(internalName).orElse(null); - if (authoritativeManifest == null) { - return false; - } String localHash = calculateHash(internalName); - String authoritativeHash = authoritativeManifest.getSha256(); + String authoritativeHash = storedVersion != null ? storedVersion.getSha256() + : authoritativeManifest != null ? authoritativeManifest.getSha256() : null; - if (localHash.isEmpty() || authoritativeHash == null || authoritativeHash.isEmpty()) { + if (Strings.isNullOrEmpty(localHash) || Strings.isNullOrEmpty(authoritativeHash)) { return false; } @@ -771,7 +780,9 @@ private void refresh() { for (String pluginName : needsDownload) { log.info("Downloading missing plugin: {}", pluginName); - downloadPlugin(pluginName); + String desiredVersion = getInstalledPluginVersion(pluginName) + .orElse(null); + downloadPlugin(pluginName, desiredVersion); } Set installedPluginNames = userManifestMap.values().stream() @@ -879,7 +890,7 @@ private void refresh() { * @param internalName the internal name of the plugin to download * @return true if the plugin was successfully downloaded, false otherwise */ - private boolean downloadPlugin(String internalName) { + private boolean downloadPlugin(String internalName, @Nullable String versionOverride) { MicrobotPluginManifest manifest = manifestMap.get(internalName); if (manifest == null) { log.error("Cannot download plugin {}: manifest not found", internalName); @@ -889,7 +900,13 @@ private boolean downloadPlugin(String internalName) { try { File pluginFile = getPluginJarFile(internalName); - HttpUrl jarUrl = microbotPluginClient.getJarURL(manifest); + String versionToDownload = !Strings.isNullOrEmpty(versionOverride) ? versionOverride : manifest.getVersion(); + if (Strings.isNullOrEmpty(versionToDownload)) { + log.error("Cannot determine version to download for {}", internalName); + return false; + } + + HttpUrl jarUrl = microbotPluginClient.getJarURL(manifest, versionToDownload); if (jarUrl == null || !jarUrl.isHttps()) { log.error("Invalid JAR URL for plugin {}", internalName); return false; @@ -909,7 +926,16 @@ private boolean downloadPlugin(String internalName) { byte[] jarData = response.body().bytes(); Files.write(jarData, pluginFile); - log.info("Plugin {} downloaded to {}", internalName, pluginFile.getAbsolutePath()); + log.info("Plugin {} (version {}) downloaded to {}", internalName, versionToDownload, pluginFile.getAbsolutePath()); + + String authoritativeHash = versionToDownload.equals(manifest.getVersion()) ? manifest.getSha256() : null; + if (Strings.isNullOrEmpty(authoritativeHash)) { + authoritativeHash = calculateHash(internalName); + } + if (!Strings.isNullOrEmpty(authoritativeHash)) { + rememberInstalledPluginVersion(internalName, versionToDownload, authoritativeHash); + } + return true; } @@ -929,8 +955,8 @@ private boolean downloadPlugin(String internalName) { * * @param manifest the manifest of the plugin to install */ - public void installPlugin(MicrobotPluginManifest manifest) { - executor.submit(() -> install(manifest)); + public void installPlugin(MicrobotPluginManifest manifest, @Nullable String versionOverride) { + executor.submit(() -> install(manifest, versionOverride)); } /** @@ -947,7 +973,7 @@ public void removePlugin(MicrobotPluginManifest manifest) { * * @param manifest the manifest of the plugin to install */ - public void install(MicrobotPluginManifest manifest) { + public void install(MicrobotPluginManifest manifest, @Nullable String versionOverride) { if (manifest == null || !manifestMap.containsValue(manifest)) { log.error("Can't install plugin: unable to identify manifest"); return; @@ -966,7 +992,7 @@ public void install(MicrobotPluginManifest manifest) { return; } - var result = downloadPlugin(internalName); + var result = downloadPlugin(internalName, versionOverride); if (result) { //verifiy hash inside loadSidePlugin doesn't work loadSideLoadPlugin(internalName); @@ -1000,6 +1026,7 @@ public void remove(MicrobotPluginManifest manifest) { return; } + File jar = getPluginJarFile(internalName); var pluginToRemove = pluginManager.getPlugins().stream().filter(x -> x.getClass().getSimpleName().equalsIgnoreCase(internalName)).findFirst(); if (pluginToRemove.isPresent()) { URLClassLoader cl = loaders.remove(internalName); @@ -1025,12 +1052,15 @@ public void remove(MicrobotPluginManifest manifest) { cl.close(); } catch (Exception ignored) { } - File jar = getPluginJarFile(internalName); - jar.delete(); } else { log.warn("Plugin to remove not found in plugin manager: {}", internalName); } + if (jar.exists() && !jar.delete()) { + log.warn("Failed to delete plugin jar {}", jar.getAbsolutePath()); + } + clearInstalledPluginVersion(internalName); + log.info("Added plugin {} to installed list", manifest.getDisplayName()); eventBus.post(new ExternalPluginsChanged()); } @@ -1040,10 +1070,10 @@ public void remove(MicrobotPluginManifest manifest) { * * @param manifest */ - public void update(MicrobotPluginManifest manifest) { + public void update(MicrobotPluginManifest manifest, @Nullable String versionOverride) { // remove and download the new one remove(manifest); - install(manifest); + install(manifest, versionOverride); } /** @@ -1067,9 +1097,9 @@ public List getInstalledPlugins() { * Submits a plugin refresh task to the executor. * This will reload plugins based on the current profile's installed plugins list. */ - public void updatePlugin(MicrobotPluginManifest manifest) { + public void updatePlugin(MicrobotPluginManifest manifest, @Nullable String versionOverride) { executor.submit(() -> { - update(manifest); + update(manifest, versionOverride); }); } @@ -1145,4 +1175,63 @@ private void shutdown() { log.error("Error during MicrobotPluginManager shutdown", e); } } + + private Optional lookupInstalledPluginVersion(String internalName) { + if (Strings.isNullOrEmpty(internalName)) { + return Optional.empty(); + } + String key = INSTALLED_VERSION_KEY_PREFIX + internalName; + String value = configManager.getConfiguration(INSTALLED_VERSION_GROUP, key); + if (Strings.isNullOrEmpty(value)) { + return Optional.empty(); + } + + String[] parts = value.split(":", 2); + if (parts.length < 2 || Strings.isNullOrEmpty(parts[0]) || Strings.isNullOrEmpty(parts[1])) { + return Optional.empty(); + } + + return Optional.of(new InstalledPluginVersion(parts[0], parts[1])); + } + + public Optional getInstalledPluginVersion(String internalName) { + return lookupInstalledPluginVersion(internalName).map(InstalledPluginVersion::getVersion); + } + + private void rememberInstalledPluginVersion(String internalName, String version, String sha256) { + if (Strings.isNullOrEmpty(internalName) || Strings.isNullOrEmpty(version) || Strings.isNullOrEmpty(sha256)) { + return; + } + + configManager.setConfiguration( + INSTALLED_VERSION_GROUP, + INSTALLED_VERSION_KEY_PREFIX + internalName, + version + ":" + sha256 + ); + } + + private void clearInstalledPluginVersion(String internalName) { + if (Strings.isNullOrEmpty(internalName)) { + return; + } + configManager.unsetConfiguration(INSTALLED_VERSION_GROUP, INSTALLED_VERSION_KEY_PREFIX + internalName); + } + + private static final class InstalledPluginVersion { + private final String version; + private final String sha256; + + private InstalledPluginVersion(String version, String sha256) { + this.version = version; + this.sha256 = sha256; + } + + public String getVersion() { + return version; + } + + public String getSha256() { + return sha256; + } + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManifest.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManifest.java index aca0944f3a1..cdf30de6ef3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManifest.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/externalplugins/MicrobotPluginManifest.java @@ -27,6 +27,10 @@ import com.google.gson.annotations.SerializedName; import lombok.Data; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Represents a plugin in the Microbot Plugin Hub */ @@ -83,17 +87,22 @@ public class MicrobotPluginManifest { */ private String cardUrl; - /** - * Flag indicating the plugin is disabled. (optional) - * This is used for plugins that are no longer functional or have been deprecated. - */ - private boolean disable; + /** + * Flag indicating the plugin is disabled. (optional) + * This is used for plugins that are no longer functional or have been deprecated. + */ + private boolean disable; - /** + /** * Tags for the plugin (optional) */ private String[] tags; + /** + * Complete version list pulled from the Microbot Nexus repository. + */ + private List availableVersions = Collections.emptyList(); + /** * Gets a warning message for this plugin, if any */ @@ -127,4 +136,19 @@ public String getAuthor() { } return String.join(", ", authors); } + + /** + * Ensures callers always receive an immutable list of versions. + */ + public List getAvailableVersions() { + return availableVersions == null ? Collections.emptyList() : availableVersions; + } + + public void setAvailableVersions(List versions) { + if (versions == null || versions.isEmpty()) { + this.availableVersions = Collections.emptyList(); + return; + } + this.availableVersions = Collections.unmodifiableList(new ArrayList<>(versions)); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java index f8d92b4fc0a..e3bfad0c6f3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/ui/MicrobotPluginHubPanel.java @@ -24,6 +24,7 @@ */ package net.runelite.client.plugins.microbot.ui; +import com.google.common.base.Strings; import com.google.common.html.HtmlEscapers; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -246,9 +247,30 @@ private class PluginItem extends JPanel implements SearchablePlugin { author.setToolTipText(authorTooltip); author.setHorizontalAlignment(JLabel.LEFT); author.setBorder(new EmptyBorder(0, 0, 0, 5)); + List availableVersions = manifest.getAvailableVersions(); JLabel version = new JLabel(currentVersion); version.setFont(FontManager.getRunescapeSmallFont()); - version.setToolTipText(currentVersion); + if (availableVersions.isEmpty()) { + version.setToolTipText(currentVersion); + } else { + String joinedVersions = availableVersions.stream() + .collect(Collectors.joining(", ")); + String tooltip = "Installed: " + HtmlEscapers.htmlEscaper().escape(currentVersion) + + "
Available: " + HtmlEscapers.htmlEscaper().escape(joinedVersions) + ""; + version.setToolTipText(tooltip); + } + String suggestedVersion = !Strings.isNullOrEmpty(manifest.getVersion()) ? manifest.getVersion() : currentVersion; + if (Strings.isNullOrEmpty(suggestedVersion)) { + suggestedVersion = "unknown"; + } + String storedVersion = microbotPluginManager.getInstalledPluginVersion(manifest.getInternalName()).orElse(null); + String initialSelectedVersion = installed ? storedVersion : null; + final VersionActionDropdown versionDropdown = new VersionActionDropdown( + manifest, + availableVersions, + initialSelectedVersion, + suggestedVersion, + installed); String descriptionText = manifest.getDescription(); @@ -292,109 +314,14 @@ private class PluginItem extends JPanel implements SearchablePlugin { configure.setVisible(false); } - JButton addrm = new JButton(); - if (!installed) { - addrm.setText("Install"); - addrm.setBackground(new Color(0x28BE28)); - addrm.addActionListener(l -> { - // Check version compatibility before installing - if (!Rs2UiHelper.isClientVersionCompatible(manifest.getMinClientVersion())) { - String _currentMicrobotVersion = RuneLiteProperties.getMicrobotVersion(); - String requiredVersion = manifest.getMinClientVersion(); - - String message = String.format( - "Cannot install plugin '%s'.\n\n" + - "Required client version: %s\n" + - "Current client version: %s\n\n" + - "Please update your Microbot client to install this plugin.", - manifest.getDisplayName(), - requiredVersion != null ? requiredVersion : "Unknown", - _currentMicrobotVersion != null ? _currentMicrobotVersion : "Unknown" - ); - // Create a custom dialog - JDialog dialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(this), "Version Incompatibility", true); - dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - dialog.setResizable(false); - dialog.setIconImages(Arrays.asList(ClientUI.ICON_128, ClientUI.ICON_16)); - - JPanel messagePanel = new JPanel(new BorderLayout(10, 0)); - messagePanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 10, 20)); - - JLabel iconLabel = new JLabel(UIManager.getIcon("OptionPane.warningIcon")); - iconLabel.setVerticalAlignment(SwingConstants.TOP); - messagePanel.add(iconLabel, BorderLayout.WEST); - - JLabel messageLabel = new JLabel("" + message.replace("\n", "
") + ""); - messageLabel.setHorizontalAlignment(SwingConstants.LEFT); - messagePanel.add(messageLabel, BorderLayout.CENTER); - - JButton okButton = new JButton("OK"); - okButton.setPreferredSize(new Dimension(67, 22)); - okButton.setBackground(ColorScheme.BRAND_ORANGE); - okButton.setForeground(ColorScheme.DARKER_GRAY_COLOR); - okButton.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createEmptyBorder(1, 1, 1, 1), - BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR, 1) - )); - okButton.setFocusPainted(false); - okButton.setFont(okButton.getFont().deriveFont(Font.BOLD)); - okButton.addActionListener(e -> dialog.dispose()); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); - buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 20, 20, 20)); - buttonPanel.add(okButton); - - dialog.setLayout(new BorderLayout()); - dialog.add(messagePanel, BorderLayout.CENTER); - dialog.add(buttonPanel, BorderLayout.SOUTH); - - dialog.pack(); - dialog.setLocationRelativeTo(this); - dialog.setVisible(true); - return; - } - - addrm.setText("Installing"); - addrm.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - microbotPluginManager.installPlugin(manifest); - }); - } else if (installed) { - // Check if update is available - boolean updateAvailable = false; - if (!loadedPlugins.isEmpty()) { - Plugin loadedPlugin = loadedPlugins.iterator().next(); - PluginDescriptor descriptor = loadedPlugin.getClass().getAnnotation(PluginDescriptor.class); - String loadedVersion = descriptor != null ? descriptor.version() : "0"; - String manifestVersion = manifest.getVersion(); - - updateAvailable = !loadedVersion.equals(manifestVersion); - } - - if (updateAvailable) { - addrm.setText("Update"); - addrm.setBackground(new Color(0x1E90FF)); // Dodger Blue - addrm.addActionListener(l -> { - addrm.setText("Updating"); - addrm.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - microbotPluginManager.updatePlugin(manifest); - reloadPluginList(); - }); - } else { - addrm.setText("Remove"); - addrm.setBackground(new Color(0xBE2828)); - addrm.addActionListener(l -> { - addrm.setText("Removing"); - addrm.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - microbotPluginManager.removePlugin(manifest); - }); - } - } else { - addrm.setText("Unavailable"); - addrm.setBackground(Color.GRAY); - addrm.setEnabled(false); - } - addrm.setBorder(new LineBorder(addrm.getBackground().darker())); - addrm.setFocusPainted(false); + GroupLayout.SequentialGroup bottomRow = layout.createSequentialGroup() + .addComponent(version, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(versionDropdown, GroupLayout.PREFERRED_SIZE, 165, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE); + bottomRow.addComponent(help, 0, 24, 24) + .addComponent(configure, 0, 24, 24) + .addGap(5); layout.setHorizontalGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup() @@ -407,15 +334,15 @@ private class PluginItem extends JPanel implements SearchablePlugin { .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) .addComponent(author)) .addComponent(description, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addComponent(version, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) - .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, 100) - .addComponent(help, 0, 24, 24) - .addComponent(configure, 0, 24, 24) - .addComponent(addrm, 0, 57, GroupLayout.PREFERRED_SIZE) - .addGap(5)))); + .addGroup(bottomRow))); int lineHeight = description.getFontMetrics(description.getFont()).getHeight(); + GroupLayout.ParallelGroup bottomRowVertical = layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(version, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(versionDropdown, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(help, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(configure, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT); + layout.setVerticalGroup(layout.createParallelGroup() .addComponent(badge, GroupLayout.Alignment.TRAILING) .addComponent(icon, HEIGHT, GroupLayout.DEFAULT_SIZE, HEIGHT + lineHeight) @@ -427,11 +354,7 @@ private class PluginItem extends JPanel implements SearchablePlugin { .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) .addComponent(description, lineHeight, GroupLayout.PREFERRED_SIZE, lineHeight * 2) .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) - .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) - .addComponent(version, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) - .addComponent(help, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) - .addComponent(configure, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) - .addComponent(addrm, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT)) + .addGroup(bottomRowVertical) .addGap(5))); } @@ -440,11 +363,256 @@ public String getSearchableName() { return manifest.getDisplayName(); } - @Override - public int installs() { - return userCount; - } - } + @Override + public int installs() { + return userCount; + } + + private final class VersionActionDropdown extends JButton + { + private final MicrobotPluginManifest manifest; + private final List versions; + private final String suggestedVersion; + private boolean installed; + private String selectedVersion; + + private VersionActionDropdown(MicrobotPluginManifest manifest, List availableVersions, + String initialSelectedVersion, String suggestedVersion, boolean installed) + { + this.manifest = manifest; + this.installed = installed; + this.suggestedVersion = suggestedVersion; + this.selectedVersion = Strings.emptyToNull(initialSelectedVersion); + this.versions = buildVersionList(availableVersions, suggestedVersion, this.selectedVersion); + setFont(FontManager.getRunescapeSmallFont()); + setFocusPainted(false); + setBorder(new LineBorder(ColorScheme.DARKER_GRAY_COLOR.brighter())); + setToolTipText("Select a version or action"); + updateButtonLabel(); + updateButtonColor(); + addActionListener(e -> showMenu()); + } + + private List buildVersionList(List availableVersions, String suggested, String selected) + { + LinkedHashSet unique = new LinkedHashSet<>(); + if (!Strings.isNullOrEmpty(selected)) + { + unique.add(selected); + } + if (!Strings.isNullOrEmpty(suggested)) + { + unique.add(suggested); + } + if (availableVersions != null) + { + for (String version : availableVersions) + { + if (!Strings.isNullOrEmpty(version)) + { + unique.add(version); + } + } + } + return new ArrayList<>(unique); + } + + private void showMenu() + { + JPopupMenu menu = new JPopupMenu(); + ButtonGroup versionGroup = new ButtonGroup(); + for (String version : versions) + { + JRadioButtonMenuItem item = new JRadioButtonMenuItem(version, version.equals(selectedVersion)); + item.addActionListener(ev -> setSelectedVersion(version, true)); + versionGroup.add(item); + menu.add(item); + } + + if (installed) + { + menu.addSeparator(); + menu.add(createActionItem("Remove", true, this::removePlugin)); + } + + menu.show(this, 0, getHeight()); + } + + private JMenuItem createActionItem(String label, boolean enabled, Runnable action) + { + JMenuItem item = new JMenuItem(label); + item.setEnabled(enabled); + if ("Remove".equalsIgnoreCase(label)) + { + item.setForeground(new Color(0xBE2828)); + } + item.addActionListener(ev -> action.run()); + return item; + } + + private void setSelectedVersion(String version, boolean triggerInstall) + { + selectedVersion = version; + updateButtonLabel(); + updateButtonColor(); + if (triggerInstall) + { + performInstallOrUpdate(); + } + } + + private void updateButtonLabel() + { + if (!Strings.isNullOrEmpty(selectedVersion)) + { + setText("Version: " + selectedVersion); + } + else if (!Strings.isNullOrEmpty(suggestedVersion)) + { + setText("Select version (" + suggestedVersion + ")"); + } + else + { + setText("Select version"); + } + } + + private void updateButtonColor() + { + Color color; + Color textColor = Color.BLACK; + if (!installed) + { + color = ColorScheme.DARKER_GRAY_COLOR; + textColor = Color.WHITE; + } + else if (hasUpdateAvailable()) + { + color = ColorScheme.BRAND_ORANGE; + } + else + { + color = new Color(0x28BE28); + } + setOpaque(true); + setBackground(color); + setForeground(textColor); + } + + private boolean hasUpdateAvailable() + { + return installed + && !Strings.isNullOrEmpty(manifest.getVersion()) + && (Strings.isNullOrEmpty(selectedVersion) || !manifest.getVersion().equals(selectedVersion)); + } + + private void performInstallOrUpdate() + { + if (Strings.isNullOrEmpty(selectedVersion)) + { + return; + } + + if (!installed) + { + installSelectedVersion(); + } + else + { + updateSelectedVersion(); + } + } + + private void installSelectedVersion() + { + if (!ensureClientVersionCompatible()) + { + return; + } + microbotPluginManager.installPlugin(manifest, selectedVersion); + installed = true; + updateButtonColor(); + } + + private void updateSelectedVersion() + { + if (!ensureClientVersionCompatible()) + { + return; + } + microbotPluginManager.updatePlugin(manifest, selectedVersion); + MicrobotPluginHubPanel.this.reloadPluginList(); + } + + private void removePlugin() + { + microbotPluginManager.removePlugin(manifest); + installed = false; + selectedVersion = null; + updateButtonLabel(); + updateButtonColor(); + } + + private boolean ensureClientVersionCompatible() + { + if (Rs2UiHelper.isClientVersionCompatible(manifest.getMinClientVersion())) + { + return true; + } + + String current = RuneLiteProperties.getMicrobotVersion(); + String required = manifest.getMinClientVersion(); + String message = String.format( + "Cannot install plugin '%s'.\n\nRequired client version: %s\nCurrent client version: %s\n\nPlease update your Microbot client to use this plugin.", + manifest.getDisplayName(), + required != null ? required : "Unknown", + current != null ? current : "Unknown" + ); + + JDialog dialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(MicrobotPluginHubPanel.this), + "Version Incompatibility", true); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dialog.setResizable(false); + dialog.setIconImages(Arrays.asList(ClientUI.ICON_128, ClientUI.ICON_16)); + + JPanel messagePanel = new JPanel(new BorderLayout(10, 0)); + messagePanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 10, 20)); + + JLabel iconLabel = new JLabel(UIManager.getIcon("OptionPane.warningIcon")); + iconLabel.setVerticalAlignment(SwingConstants.TOP); + messagePanel.add(iconLabel, BorderLayout.WEST); + + JLabel messageLabel = new JLabel("" + message.replace("\n", "
") + ""); + messageLabel.setHorizontalAlignment(SwingConstants.LEFT); + messagePanel.add(messageLabel, BorderLayout.CENTER); + + JButton okButton = new JButton("OK"); + okButton.setPreferredSize(new Dimension(67, 22)); + okButton.setBackground(ColorScheme.BRAND_ORANGE); + okButton.setForeground(ColorScheme.DARKER_GRAY_COLOR); + okButton.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(1, 1, 1, 1), + BorderFactory.createLineBorder(ColorScheme.MEDIUM_GRAY_COLOR, 1) + )); + okButton.setFocusPainted(false); + okButton.setFont(okButton.getFont().deriveFont(Font.BOLD)); + okButton.addActionListener(e -> dialog.dispose()); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 20, 20, 20)); + buttonPanel.add(okButton); + + dialog.setLayout(new BorderLayout()); + dialog.add(messagePanel, BorderLayout.CENTER); + dialog.add(buttonPanel, BorderLayout.SOUTH); + + dialog.pack(); + dialog.setLocationRelativeTo(MicrobotPluginHubPanel.this); + dialog.setVisible(true); + return false; + } + } + } private final MicrobotTopLevelConfigPanel topLevelConfigPanel; private final MicrobotPluginManager microbotPluginManager; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java index f67e90dc374..6a4b25eb926 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java @@ -140,7 +140,18 @@ public String getName() { return tileObject.getClickbox(); } - public ObjectComposition getObjectComposition() { + + @Override + public @Nullable String getOpOverride(int index) { + return ""; + } + + @Override + public boolean isOpShown(int index) { + return false; + } + + public ObjectComposition getObjectComposition() { return Microbot.getClientThread().invoke(() -> { ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); if(composition.getImpostorIds() != null) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/poh/PohPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/poh/PohPlugin.java index d24d45e4370..32fe258b5c4 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/poh/PohPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/poh/PohPlugin.java @@ -75,8 +75,8 @@ @Slf4j public class PohPlugin extends Plugin { - static final Set BURNER_UNLIT = Sets.newHashSet(ObjectID.POH_TORCH_5, ObjectID.POH_TORCH_6, ObjectID.POH_TORCH_7); - static final Set BURNER_LIT = Sets.newHashSet(ObjectID.POH_TORCH_5_LIT, ObjectID.POH_TORCH_6_LIT, ObjectID.POH_TORCH_7_LIT); + static final Set BURNER_UNLIT = Sets.newHashSet(ObjectID.POH_TORCH_5, ObjectID.POH_TORCH_6, ObjectID.POH_TORCH_7, ObjectID.POH_TORCH_8); + static final Set BURNER_LIT = Sets.newHashSet(ObjectID.POH_TORCH_5_LIT, ObjectID.POH_TORCH_6_LIT, ObjectID.POH_TORCH_7_LIT, ObjectID.POH_TORCH_8_LIT); @Getter(AccessLevel.PACKAGE) private final Map pohObjects = new HashMap<>(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/ConstructionAction.java b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/ConstructionAction.java index 43243f0ea24..5b37a806f71 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/ConstructionAction.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/ConstructionAction.java @@ -144,6 +144,7 @@ public enum ConstructionAction implements NamedSkillAction EAGLE_LECTERN("Eagle Lectern", 47, 120, ItemID.POH_LECTERN_2), SINK("Sink", 47, 300, ItemID.POH_SINK_3), MOUNTED_MYTHICAL_CAPE("Mounted Mythical Cape", 47, 370, ItemID.POH_TROPHY_MYTHICAL_CAPE), + GOLD_SINK("Gold Sink", 47, 11144, ItemID.POH_SINK_4), CHEFS_DELIGHT("Chef's Delight", 48, 224, ItemID.CHEFS_DELIGHT), TELEPORT_FOCUS("Teleport Focus", 50, 40, ItemID.POH_TELEPORT_CENTREPIECE_1), ORNAMENTAL_GLOBE("Ornamental Globe", 50, 270, ItemID.POH_GLOBE_2), @@ -193,6 +194,7 @@ public enum ConstructionAction implements NamedSkillAction MAHOGANY_WARDROBE("Mahogany Wardrobe", 75, 420, ItemID.POH_WARDROBE_6), GNOME_BENCH("Gnome Bench", 77, 840, ItemID.POH_SUPERIOR_GARDEN_BENCH_MAHOGANY), ARMILLARY_GLOBE("Armillary Globe", 77, 960, ItemID.POH_GLOBE_1), + MARBLE_WALL("Marble Wall", 79, 4000, ItemID.POH_FENCING7), MARBLE_PORTAL("Marble Portal", 80, 1500, ItemID.POH_PORTAL_FRAME_3), SCRYING_POOL("Scrying Pool", 80, 2000, ItemID.POH_TELEPORT_CENTREPIECE_3), BALANCE_BEAM("Balance Beam", 81, 1000, ItemID.POH_COMBAT_RING_5), @@ -202,6 +204,8 @@ public enum ConstructionAction implements NamedSkillAction SMALL_ORRERY("Small Orrery", 86, 1320, ItemID.POH_GLOBE_6), GILDED_WARDROBE("Gilded Wardrobe", 87, 720, ItemID.POH_WARDROBE_7), LARGE_ORRERY("Large Orrery", 95, 1420, ItemID.POH_GLOBE_7), + CRYSTAL_THRONE("Crystal Throne", 95, 15000, ItemID.POH_THRONE_6), + DEMONIC_THRONE("Demonic Throne", 99, 25000, ItemID.POH_THRONE_7), ; private final String name; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/FletchingAction.java b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/FletchingAction.java index e08c2a29514..31ffc5edcbe 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/FletchingAction.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/skillcalculator/skills/FletchingAction.java @@ -52,6 +52,14 @@ public enum FletchingAction implements ItemSkillAction IRON_JAVELIN(ItemID.IRON_JAVELIN, 17, 2), OAK_SHORTBOW(ItemID.OAK_SHORTBOW, 20, 16.5f), OAK_SHORTBOW_U(ItemID.UNSTRUNG_OAK_SHORTBOW, 20, 16.5f), + OAK_VALE_TOTEM(ItemID.ENT_TOTEMS_LOOT, 20, 254.8f) + { + @Override + public String getName(final ItemManager itemManager) + { + return "Oak vale totem"; + } + }, IRON_DART(ItemID.IRON_DART, 22, 3.8f), BLURITE_CROSSBOW(ItemID.XBOWS_CROSSBOW_BLURITE, 24, 16), OAK_STOCK(ItemID.XBOWS_CROSSBOW_STOCK_OAK, 24, 16), @@ -64,6 +72,14 @@ public enum FletchingAction implements ItemSkillAction STEEL_JAVELIN(ItemID.STEEL_JAVELIN, 32, 5), WILLOW_SHORTBOW(ItemID.WILLOW_SHORTBOW, 35, 33.3f), WILLOW_SHORTBOW_U(ItemID.UNSTRUNG_WILLOW_SHORTBOW, 35, 33.3f), + WILLOW_VALE_TOTEM(ItemID.ENT_TOTEMS_LOOT, 35, 634.4f) + { + @Override + public String getName(final ItemManager itemManager) + { + return "Willow vale totem"; + } + }, STEEL_DART(ItemID.STEEL_DART, 37, 7.5f), IRON_BOLTS(ItemID.XBOWS_CROSSBOW_BOLTS_IRON, 39, 1.5f), IRON_CROSSBOW(ItemID.XBOWS_CROSSBOW_IRON, 39, 22), @@ -84,9 +100,18 @@ public enum FletchingAction implements ItemSkillAction MITHRIL_JAVELIN(ItemID.MITHRIL_JAVELIN, 47, 8), MAPLE_SHORTBOW(ItemID.MAPLE_SHORTBOW, 50, 50), MAPLE_SHORTBOW_U(ItemID.UNSTRUNG_MAPLE_SHORTBOW, 50, 50), + MAPLE_VALE_TOTEM(ItemID.ENT_TOTEMS_LOOT, 50, 1007.2f) + { + @Override + public String getName(final ItemManager itemManager) + { + return "Maple vale totem"; + } + }, BARBED_BOLTS(ItemID.BARBED_BOLT, 51, 9.5f), BROAD_ARROWS(ItemID.SLAYERGUIDE_BROAD_ARROWS, 52, 10), MITHRIL_DART(ItemID.MITHRIL_DART, 52, 11.2f), + GREENMAN_STATUE(ItemID.GREENMAN_STATUE, 53, 55), MITHRIL_BOLTS(ItemID.XBOWS_CROSSBOW_BOLTS_MITHRIL, 54, 5), MAPLE_STOCK(ItemID.XBOWS_CROSSBOW_STOCK_MAPLE, 54, 32), MITHRIL_CROSSBOW(ItemID.XBOWS_CROSSBOW_MITHRIL, 54, 32), @@ -108,6 +133,14 @@ public enum FletchingAction implements ItemSkillAction DIAMOND_BOLTS(ItemID.XBOWS_CROSSBOW_BOLTS_ADAMANTITE_TIPPED_DIAMOND, 65, 7), YEW_SHORTBOW(ItemID.YEW_SHORTBOW, 65, 67.5f), YEW_SHORTBOW_U(ItemID.UNSTRUNG_YEW_SHORTBOW, 65, 67.5f), + YEW_VALE_TOTEM(ItemID.ENT_TOTEMS_LOOT, 65, 1635.2f) + { + @Override + public String getName(final ItemManager itemManager) + { + return "Yew vale totem"; + } + }, ADAMANT_DART(ItemID.ADAMANT_DART, 67, 15), RUNITE_BOLTS(ItemID.XBOWS_CROSSBOW_BOLTS_RUNITE, 69, 10), RUNE_CROSSBOW(ItemID.XBOWS_CROSSBOW_RUNITE, 69, 50), @@ -118,6 +151,10 @@ public enum FletchingAction implements ItemSkillAction DRAGONSTONE_BOLTS(ItemID.XBOWS_CROSSBOW_BOLTS_RUNITE_TIPPED_DRAGONSTONE, 71, 8.2f), YEW_SHIELD(ItemID.YEW_SHIELD, 72, 150), ONYX_BOLTS(ItemID.XBOWS_CROSSBOW_BOLTS_RUNITE_TIPPED_ONYX, 73, 9.4f), + ATLATL_DART_TIPS(ItemID.ATLATL_DART_TIPS, 74, 0.1f), + ATLATL_DART_SHAFT(ItemID.ATLATL_DART_SHAFT, 74, 0.3f), + HEADLESS_ATLATL_DART(ItemID.HEADLESS_ATLATL_DART, 74, 1), + ATLATL_DART(ItemID.ATLATL_DART, 74, 9.5f), RUNE_ARROW(ItemID.RUNE_ARROW, 75, 12.5f), AMETHYST_BROAD_BOLTS(ItemID.SLAYER_BROAD_BOLT_AMETHYST, 76, 10.6f), RUNE_JAVELIN(ItemID.RUNE_JAVELIN, 77, 12.4f), @@ -125,8 +162,24 @@ public enum FletchingAction implements ItemSkillAction MAGIC_STOCK(ItemID.XBOWS_CROSSBOW_STOCK_MAGIC, 78, 70), TOXIC_BLOWPIPE(ItemID.TOXIC_BLOWPIPE_LOADED, 78, 120), DRAGON_CROSSBOW_U(ItemID.XBOWS_CROSSBOW_UNSTRUNG_DRAGON, 78, 135), + GREENMAN_CARVING(ItemID.GREENMAN_WALL_DECORATION, 79, 70) + { + @Override + public String getName(final ItemManager itemManager) + { + return "Greenman carving"; + } + }, MAGIC_SHORTBOW(ItemID.MAGIC_SHORTBOW, 80, 83.3f), MAGIC_SHORTBOW_U(ItemID.UNSTRUNG_MAGIC_SHORTBOW, 80, 83.3f), + MAGIC_VALE_TOTEM(ItemID.ENT_TOTEMS_LOOT, 80, 3103.6f) + { + @Override + public String getName(final ItemManager itemManager) + { + return "Magic vale totem"; + } + }, RUNE_DART(ItemID.RUNE_DART, 81, 18.8f), AMETHYST_ARROW(ItemID.AMETHYST_ARROW, 82, 13.5f), DRAGON_BOLTS(ItemID.DRAGON_BOLTS_UNFEATHERED, 84, 12), @@ -134,8 +187,17 @@ public enum FletchingAction implements ItemSkillAction MAGIC_LONGBOW(ItemID.MAGIC_LONGBOW, 85, 91.5f), MAGIC_LONGBOW_U(ItemID.UNSTRUNG_MAGIC_LONGBOW, 85, 91.5f), MAGIC_SHIELD(ItemID.MAGIC_SHIELD, 87, 183), + REDWOOD_HIKING_STAFF(ItemID.REDWOOD_HIKING_STAFF, 90, 10.5f), DRAGON_ARROW(ItemID.DRAGON_ARROW, 90, 15), AMETHYST_DART(ItemID.AMETHYST_DART, 90, 21), + REDWOOD_VALE_TOTEM(ItemID.ENT_TOTEMS_LOOT, 90, 3787.2f) + { + @Override + public String getName(final ItemManager itemManager) + { + return "Redwood vale totem"; + } + }, DRAGON_JAVELIN(ItemID.DRAGON_JAVELIN, 92, 15), REDWOOD_SHIELD(ItemID.REDWOOD_SHIELD, 92, 216), DRAGON_DART(ItemID.DRAGON_DART, 95, 25), diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java index 13b972f579b..e54cb22e7fb 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/woodcutting/WoodcuttingPlugin.java @@ -563,6 +563,12 @@ public void onScriptPreFired(ScriptPreFired scriptPreFired) case ObjectID.FARMING_REDWOOD_TREE_PATCH_1_4: case ObjectID.FARMING_REDWOOD_TREE_PATCH_1_6: case ObjectID.FARMING_REDWOOD_TREE_PATCH_1_8: + + // sailing trees + case ObjectID.JATOBA_TREE_STUMP: + case ObjectID.CAMPHOR_TREE_UPDATE_STUMP: + case ObjectID.IRONWOOD_TREE_UPDATE_STUMP: + case ObjectID.ROSEWOOD_TREE_UPDATE_STUMP: { WorldPoint worldPoint = WorldPoint.fromCoord(locCoord); GameObject gameObject = findObject(worldPoint); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/MapPoint.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/MapPoint.java index 161c669604a..41e74ed2244 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/MapPoint.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/MapPoint.java @@ -48,7 +48,9 @@ enum Type AGILITY_COURSE, AGILITY_SHORTCUT, QUEST, - RARE_TREE + RARE_TREE, + MOORING_POINT, + ; } @Getter diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/MooringLocation.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/MooringLocation.java new file mode 100644 index 00000000000..c6a86c30aba --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/MooringLocation.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025, coopermor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.worldmap; + +import lombok.Getter; +import net.runelite.api.coords.WorldPoint; + +@Getter +public enum MooringLocation +{ + PORT_SARIM("Port Sarim", 1, new WorldPoint(3050, 3192, 0)), + THE_PANDEMONIUM("The Pandemonium", 1, new WorldPoint(3069, 2986, 0)), + LANDS_END("Land's End", 5, new WorldPoint(1506, 3402, 0)), + HOSIDIUS("Hosidius", 5, new WorldPoint(1726, 3452, 0)), + MUSA_POINT("Musa Point", 10, new WorldPoint(2960, 3147, 0)), + PORT_PISCARILLIUS("Port Piscarillius", 15, new WorldPoint(1845, 3687, 0)), + RIMMINGTON("Rimmington", 18, new WorldPoint(2905, 3226, 0)), + CATHERBY("Catherby", 20, new WorldPoint(2796, 3412, 0)), + BRIMHAVEN("Brimhaven", 25, new WorldPoint(2757, 3229, 0)), + ARDOUGNE("Ardougne", 28, new WorldPoint(2671, 3265, 0)), + PORT_KHAZARD("Port Khazard", 30, new WorldPoint(2685, 3161, 0)), + WITCHAVEN("Witchaven", 34, new WorldPoint(2746, 3304, 0)), + ENTRANA("Entrana", 36, new WorldPoint(2878, 3335, 0)), + CIVITAS_ILLA_FORTIS("Civitas illa Fortis", 38, new WorldPoint(1774, 3141, 0)), + CORSAIR_COVE("Corsair Cove", 40, new WorldPoint(2579, 2843, 0)), + DOGNOSE_ISLAND("Dognose Island", 40, new WorldPoint(3061, 2639, 0)), + CAIRN_ISLE("Cairn Isle", 42, new WorldPoint(2749, 2951, 0)), + CHINCHOMPA_ISLAND("Chinchompa Island", 42, new WorldPoint(1892, 3429, 0)), + SUNSET_COAST("Sunset Coast", 44, new WorldPoint(1511, 2975, 0)), + REMOTE_ISLAND("Remote Island", 45, new WorldPoint(2971, 2603, 0)), + THE_SUMMER_SHORE("The Summer Shore", 45, new WorldPoint(3174, 2367, 0)), + THE_LITTLE_PEARL("The Little Pearl", 45, new WorldPoint(3354, 2216, 0)), + ALDARIN("Aldarin", 46, new WorldPoint(1452, 2970, 0)), + VATRACHOS_ISLAND("Vatrachos Island", 46, new WorldPoint(1872, 2985, 0)), + THE_ONYX_CREST("The Onyx Crest", 47, new WorldPoint(2997, 2288, 0)), + RUINS_OF_UNKAH("Ruins of Unkah", 48, new WorldPoint(3143, 2824, 0)), + SHIMMERING_ATOLL("Shimmering Atoll", 49, new WorldPoint(1557, 2771, 0)), + VOID_KNIGHTS_OUTPOST("Void Knights' Outpost", 50, new WorldPoint(2651, 2678, 0)), + PORT_ROBERTS("Port Roberts", 50, new WorldPoint(1860, 3306, 0)), + ANGLERS_RETREAT("Anglers' Retreat", 51, new WorldPoint(2467, 2721, 0)), + MINOTAURS_REST("Minotaurs' Rest", 54, new WorldPoint(1958, 3117, 0)), + ISLE_OF_SOULS("Isle of Souls", 55, new WorldPoint(2282, 2823, 0)), + ISLE_OF_BONES("Isle of Bones", 56, new WorldPoint(2532, 2531, 0)), + LAGUNA_AURORAE("Laguna Aurorae", 58, new WorldPoint(1202, 2733, 0)), + CHARRED_ISLAND("Charred Island", 60, new WorldPoint(2660, 2395, 0)), + TEAR_OF_THE_SOUL("Tear of the Soul", 61, new WorldPoint(2318, 2774, 0)), + RELLEKKA("Rellekka", 62, new WorldPoint(2630, 3705, 0)), + WINTUMBER_ISLAND("Wintumber Island", 63, new WorldPoint(2058, 2606, 0)), + THE_CROWN_JEWEL("The Crown Jewel", 64, new WorldPoint(1765, 2659, 0)), + ETCETERIA("Etceteria", 65, new WorldPoint(2611, 3840, 0)), + PORT_TYRAS("Port Tyras", 66, new WorldPoint(2144, 3120, 0)), + LLEDRITH_ISLAND("Lledrith Island", 66, new WorldPoint(2097, 3188, 0)), + DEEPFIN_POINT("Deepfin Point", 67, new WorldPoint(1923, 2758, 0)), + JATIZSO("Jatizso", 68, new WorldPoint(2412, 3780, 0)), + NETIZNOT("Netiznot", 68, new WorldPoint(2308, 3783, 0)), + RAINBOWS_END("Rainbow's End", 69, new WorldPoint(2344, 2270, 0)), + PRIFDDINAS("Prifddinas", 70, new WorldPoint(2158, 3324, 0)), + SUNBLEAK_ISLAND("Sunbleak Island", 72, new WorldPoint(2189, 2327, 0)), + YNYSDAIL("Ynysdail", 73, new WorldPoint(2222, 3466, 0)), + WATERBIRTH_ISLAND("Waterbirth Island", 74, new WorldPoint(2543, 3765, 0)), + PISCATORIS("Piscatoris", 75, new WorldPoint(2303, 3690, 0)), + LUNAR_ISLE("Lunar Isle", 76, new WorldPoint(2151, 3880, 0)), + BUCCANEERS_HAVEN("Buccaneers' Haven", 76, new WorldPoint(2080, 3690, 0)), + DRUMSTICK_ISLE("Drumstick Isle", 79, new WorldPoint(2150, 3530, 0)), + WEISS("Weiss", 80, new WorldPoint(2860, 3972, 0)), + BRITTLE_ISLE("Brittle Isle", 81, new WorldPoint(1954, 4056, 0)), + GRIMSTONE("Grimstone", 87, new WorldPoint(2927, 4056, 0)), + ; + + private final String tooltip; + private final int levelReq; + private final WorldPoint location; + + MooringLocation(String description, int level, WorldPoint location) + { + this.tooltip = description + " - Level " + level; + this.location = location; + this.levelReq = level; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapConfig.java index abc6548c197..a989de626bc 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapConfig.java @@ -295,4 +295,26 @@ default boolean fishingSpotTooltips() { return true; } + + @ConfigItem( + keyName = WorldMapPlugin.CONFIG_KEY_MOORING_LOCATION_TOOLTIPS, + name = "Mooring location tooltips", + description = "Indicates the level required to moor at a location.", + position = 25 + ) + default boolean mooringLocationTooltips() + { + return true; + } + + @ConfigItem( + keyName = WorldMapPlugin.CONFIG_KEY_MOORING_LOCATION_LEVEL_ICON, + name = "Indicate inaccessible mooring locations", + description = "Indicate mooring points you do not have the level to use on the icon.", + position = 26 + ) + default boolean mooringPointLevelIcon() + { + return true; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapPlugin.java index 64e9c47672c..6979831b00d 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/worldmap/WorldMapPlugin.java @@ -105,6 +105,8 @@ public class WorldMapPlugin extends Plugin static final String CONFIG_KEY_DUNGEON_TOOLTIPS = "dungeonTooltips"; static final String CONFIG_KEY_HUNTER_AREA_TOOLTIPS = "hunterAreaTooltips"; static final String CONFIG_KEY_FISHING_SPOT_TOOLTIPS = "fishingSpotTooltips"; + static final String CONFIG_KEY_MOORING_LOCATION_TOOLTIPS = "mooringLocationTooltips"; + static final String CONFIG_KEY_MOORING_LOCATION_LEVEL_ICON = "mooringLocationShortcutIcon"; static { @@ -161,6 +163,7 @@ public class WorldMapPlugin extends Plugin private int agilityLevel = 0; private int woodcuttingLevel = 0; + private int sailingLevel = 0; private final Map questStartLocations = new EnumMap<>(Quest.class); @@ -175,6 +178,7 @@ protected void startUp() throws Exception { agilityLevel = client.getRealSkillLevel(Skill.AGILITY); woodcuttingLevel = client.getRealSkillLevel(Skill.WOODCUTTING); + sailingLevel = client.getRealSkillLevel(Skill.SAILING); updateShownIcons(); } @@ -185,6 +189,7 @@ protected void shutDown() throws Exception questStartLocations.clear(); agilityLevel = 0; woodcuttingLevel = 0; + sailingLevel = 0; } @Subscribe @@ -223,6 +228,17 @@ public void onStatChanged(StatChanged statChanged) } break; } + case SAILING: + { + // Docking at locations is not boostable + int newSailingLevel = client.getRealSkillLevel(Skill.SAILING); + if (newSailingLevel != sailingLevel) + { + sailingLevel = newSailingLevel; + updateMooringPointIcons(); + } + break; + } } } @@ -361,10 +377,30 @@ private void updateRareTreeIcons() } } + private void updateMooringPointIcons() + { + worldMapPointManager.removeIf(isType(MapPoint.Type.MOORING_POINT)); + + if (config.mooringLocationTooltips() || config.mooringPointLevelIcon()) + { + Arrays.stream(MooringLocation.values()) + .map(l -> + MapPoint.builder() + .type(MapPoint.Type.MOORING_POINT) + .worldPoint(l.getLocation()) + .image(sailingLevel > 0 && config.mooringPointLevelIcon() && l.getLevelReq() > sailingLevel ? NOPE_ICON : BLANK_ICON) + .tooltip(config.mooringLocationTooltips() ? l.getTooltip() : null) + .build() + ) + .forEach(worldMapPointManager::add); + } + } + private void updateShownIcons() { updateAgilityIcons(); updateAgilityCourseIcons(); + updateMooringPointIcons(); updateRareTreeIcons(); updateQuestStartPointIcons(); diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java index 4b8eb4ff376..f1e947e6c1f 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java @@ -30,6 +30,7 @@ import java.awt.Composite; import java.awt.Cursor; import java.awt.Dimension; +import java.awt.Font; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Point; @@ -96,6 +97,8 @@ public class OverlayRenderer extends MouseAdapter private final EventBus eventBus; private final ChatMessageManager chatMessageManager; + private Font font, tooltipFont, interfaceFont; + // Overlay movement variables private final Point overlayOffset = new Point(); private final Point mousePosition = new Point(); @@ -288,6 +291,11 @@ private void renderOverlays(final Graphics2D graphics, Collection overl final RenderingHints renderingHints = graphics.getRenderingHints(); final Color background = graphics.getBackground(); + // Cache overlay fonts + this.font = runeLiteConfig.fontType().getFont(); + this.tooltipFont = runeLiteConfig.tooltipFontType().getFont(); + this.interfaceFont = runeLiteConfig.interfaceFontType().getFont(); + final Rectangle clip = clipBounds(layer); graphics.setClip(clip); @@ -714,15 +722,15 @@ private void safeRender(Overlay overlay, Graphics2D graphics, Point point) // Set font based on configuration if (position == OverlayPosition.DYNAMIC || position == OverlayPosition.DETACHED) { - graphics.setFont(runeLiteConfig.fontType().getFont()); + graphics.setFont(font); } else if (position == OverlayPosition.TOOLTIP) { - graphics.setFont(runeLiteConfig.tooltipFontType().getFont()); + graphics.setFont(tooltipFont); } else { - graphics.setFont(runeLiteConfig.interfaceFontType().getFont()); + graphics.setFont(interfaceFont); } graphics.translate(point.x, point.y); diff --git a/runelite-client/src/main/java/net/runelite/client/util/GameEventManager.java b/runelite-client/src/main/java/net/runelite/client/util/GameEventManager.java index b72e97913ed..3e3192fc397 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/GameEventManager.java +++ b/runelite-client/src/main/java/net/runelite/client/util/GameEventManager.java @@ -42,6 +42,7 @@ import net.runelite.api.Tile; import net.runelite.api.TileItem; import net.runelite.api.WallObject; +import net.runelite.api.WorldEntity; import net.runelite.api.WorldView; import net.runelite.api.events.DecorativeObjectSpawned; import net.runelite.api.events.GameObjectSpawned; @@ -51,6 +52,7 @@ import net.runelite.api.events.NpcSpawned; import net.runelite.api.events.PlayerSpawned; import net.runelite.api.events.WallObjectSpawned; +import net.runelite.api.events.WorldEntitySpawned; import net.runelite.client.callback.ClientThread; import net.runelite.client.eventbus.EventBus; @@ -207,9 +209,10 @@ private void simulateGameEvents(WorldView wv) } }); - for (WorldView sub : wv.worldViews()) + for (WorldEntity we : wv.worldEntities()) { - simulateGameEvents(sub); + eventBus.post(new WorldEntitySpawned(we)); + simulateGameEvents(we.getWorldView()); } } } \ No newline at end of file diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/ships.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/ships.tsv index 027d8474bb7..6e5e350476e 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/ships.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/ships.tsv @@ -3,11 +3,11 @@ 3029 3217 0 2956 3143 1 Pay-fare;Captain Tobias;3644 30 Coins 10 Musa Point 2956 3146 0 3032 3217 1 Pay-Fare;Customs officer;3648 30 Coins 10 Port Sarim # Ardougne, Brimhaven, Rimmington -2683 3271 0 2775 3233 1 Brimhaven;Captain Barnaby;9250 30 Coins Y 6 Brimhaven -2683 3271 0 2915 3221 1 Rimmington;Captain Barnaby;9250 30 Coins Y 6 Rimmington -2772 3234 0 2683 3268 1 Ardougne;Captain Barnaby;8764 30 Coins Y 6 Ardougne +2673 3275 0 2775 3233 1 Brimhaven;Captain Barnaby;9250 30 Coins Y 6 Brimhaven +2673 3275 0 2915 3221 1 Rimmington;Captain Barnaby;9250 30 Coins Y 6 Rimmington +2772 3234 0 2683 3263 1 Ardougne;Captain Barnaby;8764 30 Coins Y 6 Ardougne 2772 3234 0 2915 3221 1 Rimmington;Captain Barnaby;8764 30 Coins Y 6 Rimmington -2915 3225 0 2683 3268 1 Ardougne;Captain Barnaby;8763 30 Coins Y 6 Ardougne +2915 3225 0 2683 3263 1 Ardougne;Captain Barnaby;8763 30 Coins Y 6 Ardougne 2915 3225 0 2775 3233 1 Brimhaven;Captain Barnaby;8763 30 Coins Y 6 Brimhaven # Rimmington, Corsair Cove 2910 3226 0 2578 2837 1 Travel;Cabin Boy Colin;7967 The Corsair Curse 6 Corsair Cove diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv index aa5f7602a2a..9a748fa59e9 100644 --- a/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv +++ b/runelite-client/src/main/resources/net/runelite/client/plugins/microbot/shortestpath/transports.tsv @@ -600,9 +600,7 @@ 2650 3282 0 2650 3282 1 Climb-up;Ladder;16683 2 2650 3282 1 2650 3282 0 Climb-down;Ladder;16679 2 2649 3282 1 2649 3282 2 Climb-up;Ladder;17026 2 -2649 3282 2 2649 3282 1 Climb-down;Ladder;16685 2 -2683 3271 0 2683 3268 1 Cross;Gangplank;2085 3 -2683 3268 1 2683 3271 0 Cross;Gangplank;2086 3 +2649 3282 2 2649 3282 1 Climb-down;Ladder;16685 2 2622 3291 0 2622 3291 1 Climb-up;Ladder;17026 2 2622 3291 1 2622 3291 0 Climb-down;Ladder;16685 2 2592 3339 0 2592 3338 0 Open;Door;2054 1 @@ -653,7 +651,9 @@ 2674 3305 0 2674 3306 0 Pick-lock;Door;11719 2 2674 3303 0 2674 3304 0 Open;Door;11720 1 2674 3306 0 2674 3305 0 Open;Door;11719 1 - +2683 3263 1 2683 3266 0 Cross;Gangplank;2086 1 +2683 3266 0 2683 3263 1 Cross;Gangplank;2085 1 + # Witchaven Dungeon 2697 3283 0 2696 9683 0 Climb-down;Old ruin entrance;18270 2696 9683 0 2697 3283 0 Climb-up;Exit;18354 diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java index 88c948c5188..e61158837d6 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/loottracker/LootTrackerPluginTest.java @@ -669,4 +669,21 @@ public void testZombiePirateLockerLoot() new ItemStack(ItemID.RUNE_SWORD, 2) )); } + + @Test + public void testLargeSalvage() + { + ItemContainer itemContainer = mock(ItemContainer.class); + when(itemContainer.getItems()).thenReturn(new Item[]{new Item(ItemID.SAILING_LARGE_SHIPWRECK_SALVAGE, 1)}); + when(client.getItemContainer(InventoryID.INV)).thenReturn(itemContainer); + + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.GAMEMESSAGE, "", "You sort through the large salvage and find: 4 x Steel nails.", "", 0); + lootTrackerPlugin.onChatMessage(chatMessage); + + when(itemContainer.getItems()).thenReturn(new Item[]{new Item(ItemID.NAILS, 4)}); + + lootTrackerPlugin.onItemContainerChanged(new ItemContainerChanged(InventoryID.INV, itemContainer)); + + verify(lootTrackerPlugin).addLoot("Large salvage", -1, LootRecordType.EVENT, null, Collections.singletonList(new ItemStack(ItemID.NAILS, 4))); + } }