From 479d9bd3a212e4264c4b1e109a22719898c2c4af Mon Sep 17 00:00:00 2001 From: kasperknudas Date: Thu, 7 Aug 2025 11:38:44 +0200 Subject: [PATCH 1/3] Import of PowerSkillerPlugin and configured gradle files and dependencies accordingly --- PowerSkiller/build.gradle | 42 +++ .../java/com/example/PowerSkillerConfig.java | 167 ++++++++++ .../java/com/example/PowerSkillerOverlay.java | 53 +++ .../java/com/example/PowerSkillerPlugin.java | 309 ++++++++++++++++++ .../src/main/java/com/example/State.java | 10 + build.gradle | 2 + settings.gradle | 2 + .../java/com/example/ExamplePluginTest.java | 4 +- 8 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 PowerSkiller/build.gradle create mode 100644 PowerSkiller/src/main/java/com/example/PowerSkillerConfig.java create mode 100644 PowerSkiller/src/main/java/com/example/PowerSkillerOverlay.java create mode 100644 PowerSkiller/src/main/java/com/example/PowerSkillerPlugin.java create mode 100644 PowerSkiller/src/main/java/com/example/State.java diff --git a/PowerSkiller/build.gradle b/PowerSkiller/build.gradle new file mode 100644 index 00000000..eb88c2d2 --- /dev/null +++ b/PowerSkiller/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' +} + +group = 'com.example' +version = '1.0-SNAPSHOT' + + +repositories { + mavenLocal() // Important: so PacketUtils can be found + mavenCentral() + maven { + url = 'https://repo.runelite.net' + content { + includeGroupByRegex("net\\.runelite.*") + } + } +} + +def runeLiteVersion = '1.11.14-SNAPSHOT' // Match what you built and installed from the RuneLite repo + +dependencies { + // RuneLite core API + compileOnly "net.runelite:client:$runeLiteVersion" + compileOnly "net.runelite:runelite-api:$runeLiteVersion" + + // Your locally published EthanAPI / PacketUtils + implementation 'com.ethanvann:packetutils:5.4' + + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} \ No newline at end of file diff --git a/PowerSkiller/src/main/java/com/example/PowerSkillerConfig.java b/PowerSkiller/src/main/java/com/example/PowerSkillerConfig.java new file mode 100644 index 00000000..f9310154 --- /dev/null +++ b/PowerSkiller/src/main/java/com/example/PowerSkillerConfig.java @@ -0,0 +1,167 @@ +package com.example; + +import net.runelite.client.config.*; + +@ConfigGroup("PowerSkiller") +public interface PowerSkillerConfig extends Config { + @ConfigSection( + name = "Tick Delays", + description = "Configuration for delays added to skilling activities", + position = 3 + + ) + String tickDelaySection = "Tick Delays"; + @ConfigSection( + name = "Drop Config", + description = "Configuration for amount of items to drop", + position = 5 + + ) + String dropConfigSection = "Drop Config"; + + @ConfigItem( + keyName = "toggle", + name = "Toggle", + description = "", + position = -2 + ) + default Keybind toggle() { + return Keybind.NOT_SET; + } + + + @ConfigItem( + name = "Should Bank?", + keyName = "shouldBank", + description = "Search for a bank in the nearby area, if found bank when full", + position = -3 + ) + default boolean shouldBank() { + return false; + } + + @ConfigItem( + keyName = "searchNpc", + name = "Search NPCs (for fishing, etc)", + description = "For things like fishing spots", + position = -1 + ) + default boolean searchNpc() { + return false; + } + + @ConfigItem( + name = "Object", + keyName = "objectToInteract", + description = "Game obejct you will be interacting with", + position = 0 + ) + default String objectToInteract() { + return "Tree"; + } + + @ConfigItem( + name = "Expected Action", + keyName = "Expected Action", + description = "The action you wish to do on the object", + position = 1 + ) + default String expectedAction() { + return "Chop"; + } + + @ConfigItem( + name = "Tool(s)", + keyName = "toolsToUse", + description = "Tools required to act with your object, can type ` axe` or ` pickaxe` to ignore the type", + position = 2 + ) + default String toolsToUse() { + return " axe"; + } + + @ConfigItem( + name = "Keep Items", + keyName = "itemToKeep", + description = "Items you don't want dropped. Separate items by comma,no space. Good for UIM", + position = 3 + ) + default String itemsToKeep() { + return "coins,rune pouch,divine rune pouch,looting bag,clue scroll"; + } + + + @ConfigItem( + name = "Tick Delay Min", + keyName = "tickDelayMin", + description = "Lower bound of tick delay, can set both to 0 to remove delay", + position = 4, + section = tickDelaySection + ) + default int tickdelayMin() { + return 0; + } + + @Range( + + ) + @ConfigItem( + name = "Tick Delay Max", + keyName = "tickDelayMax", + description = "Upper bound of tick delay, can set both to 0 to remove delay", + position = 5, + section = tickDelaySection + ) + default int tickDelayMax() { + return 3; + } + + + @Range( + max = 9 + ) + @ConfigItem( + name = "Drop Per Tick Min", + keyName = "numToDrop1", + description = "Minimum amount of items dropped per tick", + position = 6, + section = dropConfigSection + ) + default int dropPerTickOne() { + return 1; + } + + @Range( + max = 9 + ) + @ConfigItem( + name = "Drop Per Tick Max", + keyName = "numToDrop2", + description = "Maximum amount of items dropped per tick", + position = 7, + section = dropConfigSection + ) + default int dropPerTickTwo() { + return 3; + } + @ConfigItem( + name = "Forestry Tree", + keyName = "dropItems", + description = "Object w most players,UNCHECK IF NOT WC", + position = 5 + ) + default boolean useForestryTreeNotClosest() { + return false; + } + +// Artifact from when hutch wrote everything for dirty UIMs. we keep cuz its funny. +// @ConfigItem( +// name = "Empty slots", +// keyName = "emptySlots", +// description = "Amount of empty slots you have to skill with, mostly a UIM feature lol", +// position = 3 +// ) +// default int emptySlots() { +// return 28; +// } +} \ No newline at end of file diff --git a/PowerSkiller/src/main/java/com/example/PowerSkillerOverlay.java b/PowerSkiller/src/main/java/com/example/PowerSkillerOverlay.java new file mode 100644 index 00000000..037f3498 --- /dev/null +++ b/PowerSkiller/src/main/java/com/example/PowerSkillerOverlay.java @@ -0,0 +1,53 @@ +package com.example; + +import com.google.inject.Inject; +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 java.awt.*; + +public class PowerSkillerOverlay extends OverlayPanel { + + private final PowerSkillerPlugin plugin; + + @Inject + private PowerSkillerOverlay(PowerSkillerPlugin plugin) { + super(plugin); + this.plugin = plugin; + setPosition(OverlayPosition.BOTTOM_LEFT); + setPreferredSize(new Dimension(160, 160)); + } + + + @Override + public Dimension render(Graphics2D graphics) { + panelComponent.setPreferredSize(new Dimension(200, 320)); + panelComponent.getChildren().add(TitleComponent.builder() + .text("Piggy Power Skiller") + .color(new Color(255, 157, 249)) + .build()); + panelComponent.getChildren().add(TitleComponent.builder() + .text(plugin.started ? "Running" : "Paused") + .color(plugin.started ? Color.GREEN : Color.RED) + .build()); + panelComponent.getChildren().add(LineComponent.builder() + .left("State: ") + .leftColor(new Color(255, 157, 249)) + .right(plugin.state==null || !plugin.started ? "STOPPED" : plugin.state.name()) + .rightColor(Color.WHITE) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Trying to: ") + .leftColor(new Color(255, 157, 249)) + .right(plugin.config.expectedAction() +" " + plugin.config.objectToInteract() ) + .rightColor(Color.WHITE) + .build()); + + + + return super.render(graphics); + } +} diff --git a/PowerSkiller/src/main/java/com/example/PowerSkillerPlugin.java b/PowerSkiller/src/main/java/com/example/PowerSkillerPlugin.java new file mode 100644 index 00000000..bffa445e --- /dev/null +++ b/PowerSkiller/src/main/java/com/example/PowerSkillerPlugin.java @@ -0,0 +1,309 @@ +package com.example; + +import com.example.EthanApiPlugin.Collections.*; +import com.example.EthanApiPlugin.Collections.query.TileObjectQuery; +import com.example.EthanApiPlugin.EthanApiPlugin; +import com.example.InteractionApi.BankInventoryInteraction; +import com.example.InteractionApi.InventoryInteraction; +import com.example.InteractionApi.NPCInteraction; +import com.example.InteractionApi.TileObjectInteraction; +import com.google.inject.Provides; +import net.runelite.api.*; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.api.widgets.Widget; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.input.KeyManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.util.HotkeyListener; + +import com.google.inject.Inject; +import net.runelite.client.util.Text; +import org.apache.commons.lang3.RandomUtils; + +import java.util.*; + + +@PluginDescriptor( + name = "[PP] Power Skiller", + description = "Will interact with an object and drop or bank all items when inventory is full", + tags = {"ethan", "piggy", "skilling"} +) +public class PowerSkillerPlugin extends Plugin { + @Inject + private Client client; + @Inject + PowerSkillerConfig config; + @Inject + private KeyManager keyManager; + @Inject + private OverlayManager overlayManager; + @Inject + private PowerSkillerOverlay overlay; + State state; + boolean started; + + boolean bankPin = false; + + private int timeout; + + @Override + protected void startUp() throws Exception { + bankPin = false; + keyManager.registerKeyListener(toggle); + this.overlayManager.add(overlay); + } + + @Override + protected void shutDown() throws Exception { + bankPin = false; + keyManager.unregisterKeyListener(toggle); + this.overlayManager.remove(overlay); + } + + @Provides + private PowerSkillerConfig getConfig(ConfigManager configManager) { + return configManager.getConfig(PowerSkillerConfig.class); + } + + @Subscribe + private void onGameTick(GameTick event) { + if (!EthanApiPlugin.loggedIn() || !started) { + // We do an early return if the user isn't logged in + return; + } + state = getNextState(); + handleState(); + } + + private void handleState() { + switch (state) { + case BANK: + if (Widgets.search().withId(13959169).first().isPresent()) { + bankPin = true; + return; + } + if (Widgets.search().withId(786445).first().isEmpty()) { + TileObjects.search().withAction("Bank").nearestToPlayer().ifPresent(tileObject -> { + TileObjectInteraction.interact(tileObject, "Bank"); + return; + }); + /* Outdated - check to fix later. + NPCs.search().withAction("Bank").nearestToPlayer().ifPresent(npc -> { + if (EthanApiPlugin.pathToGoal(npc.getWorldLocation(), new HashSet<>()) != null) { + NPCInteraction.interact(npc, "Bank"); + } + return; + }); */ + TileObjects.search().withName("Bank chest").nearestToPlayer().ifPresent(tileObject -> { + TileObjectInteraction.interact(tileObject, "Use"); + return; + }); + if (TileObjects.search().withAction("Bank").nearestToPlayer().isEmpty() && NPCs.search().withAction("Bank").nearestToPlayer().isEmpty()) { + EthanApiPlugin.sendClientMessage("Bank is not found, move to an area with a bank."); + } + + return; + + } + List items = BankInventory.search().result(); + for (Widget item : items) { + if (!isTool(item.getName().toLowerCase()) && !shouldKeep(item.getName().toLowerCase())) { + BankInventoryInteraction.useItem(item, "Deposit-All"); + return; + } + } + break; + case TIMEOUT: + timeout--; + break; + case FIND_OBJECT: + if (config.searchNpc()) { + findNpc(); + } else { + findObject(); + } + setTimeout(); + break; + case DROP_ITEMS: + dropItems(); + break; + } + } + + private State getNextState() { + // self-explanatory, we just return a State if the conditions are met. + if (EthanApiPlugin.isMoving() || client.getLocalPlayer().getAnimation() != -1) { + // this is to prevent clicks while animating/moving. + return State.ANIMATING; + } + + if (!hasTools()) { + // if the user doesn't have tools we don't want it to do anything at all lol, maybe stop the plugin if you want. + return State.MISSING_TOOLS; + } + if (timeout > 0) { + return State.TIMEOUT; + } + + if (shouldBank() && Inventory.full() || (Bank.isOpen() && !isInventoryReset())) { + if (shouldBank() && !isInventoryReset()) { + return State.BANK; + } + } + + if ((isDroppingItems() && !isInventoryReset()) || !shouldBank() && Inventory.full()) { + // if the user should be dropping items, we'll check if they're done + // should sit at this state til it's finished. + return State.DROP_ITEMS; + } + + + // default it'll look for an object. + return State.FIND_OBJECT; + } + + private void findObject() { + String objectName = config.objectToInteract(); + if (config.useForestryTreeNotClosest() && config.expectedAction().equalsIgnoreCase("chop")) { + TileObjects.search().withName(objectName).nearestToPoint(getObjectWMostPlayers()).ifPresent(tileObject -> { + ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); + TileObjectInteraction.interact(tileObject, comp.getActions()[0]); + }); + } else { + TileObjects.search().withName(objectName).nearestToPlayer().ifPresent(tileObject -> { + ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); + TileObjectInteraction.interact(tileObject, comp.getActions()[0]); // find the object we're looking for. this specific example will only work if the first Action the object has is the one that interacts with it. + // don't *always* do this, you can manually type the possible actions. eg. "Mine", "Chop", "Cook", "Climb". + }); + } + } + + /** + * Tile w most players on it within 2 tiles of the object we're looking for + * + * @return That tile or the player's tile if failed(such as doing forestry option when alone by trees) + */ + public WorldPoint getObjectWMostPlayers() { + String objectName = config.objectToInteract(); + Map playerCounts = new HashMap<>(); + WorldPoint mostPlayersTile = null; + int highestCount = 0; + List objects = TileObjects.search().withName(objectName).result(); + + List players = Players.search().notLocalPlayer().result(); + + for (TileObject object : objects) { + for (Player player : players) { + if (player.getWorldLocation().distanceTo(object.getWorldLocation()) <= 2) { + WorldPoint playerTile = player.getWorldLocation(); + playerCounts.put(playerTile, playerCounts.getOrDefault(playerTile, 0) + 1); + if (playerCounts.get(playerTile) > highestCount) { + highestCount = playerCounts.get(playerTile); + mostPlayersTile = playerTile; + } + } + } + } + + return mostPlayersTile == null ? client.getLocalPlayer().getWorldLocation() : mostPlayersTile; + } + + + private void findNpc() { + String npcName = config.objectToInteract(); + NPCs.search().withName(npcName).nearestToPlayer().ifPresent(npc -> { + NPCComposition comp = client.getNpcDefinition(npc.getId()); + if (Arrays.stream(comp.getActions()).anyMatch(action -> action.equalsIgnoreCase(config.expectedAction()))) { + NPCInteraction.interact(npc, config.expectedAction()); // For fishing spots ? + } else { + NPCInteraction.interact(npc, comp.getActions()[0]); + } + + }); + } + + private void dropItems() { + List itemsToDrop = Inventory.search() + .filter(item -> !shouldKeep(item.getName()) && !isTool(item.getName())).result(); // filter the inventory to only get the items we want to drop + + for (int i = 0; i < Math.min(itemsToDrop.size(), RandomUtils.nextInt(config.dropPerTickOne(), config.dropPerTickTwo())); i++) { + InventoryInteraction.useItem(itemsToDrop.get(i), "Drop"); // we'll loop through this at a max of 10 times. can make this a config options. drops x items per tick (x = 10 in this example) + } + } + + private boolean isInventoryReset() { + List inventory = Inventory.search().result(); + for (Widget item : inventory) { + if (!shouldKeep(Text.removeTags(item.getName()))) { // using our shouldKeep method, we can filter the items here to only include the ones we want to drop. + return false; + } + } + return true; // we will know that the inventory is reset because the inventory only contains items we want to keep + } + + private boolean isDroppingItems() { + return state == State.DROP_ITEMS; // if the user is dropping items, we don't want it to proceed until they're all dropped. + } + + + private boolean shouldKeep(String name) { + List itemsToKeep = new ArrayList<>(List.of(config.itemsToKeep().split(","))); // split the items listed by comma. and add them to a list. + itemsToKeep.addAll(List.of(config.toolsToUse().split(","))); //We must also check if the tools are included in the Inventory, Rather than equipped, so they are added here + return itemsToKeep.stream()// stream the List using Collection.stream() from java.util + .anyMatch(i -> Text.removeTags(name.toLowerCase()).contains(i.toLowerCase())); + // we'll set everything to lowercase as well as remove the html tags that is included (The color of the item in game), + // and check if the input name contains any of the items in the itemsToKeep list. + // might seem silly, but this is to allow specific items you want to keep without typing the full name. + // We also prefer names to ids here, but you can change this if you like. + } + + private boolean hasTools() { + //Updated from https://github.com/moneyprinterbrrr/ImpactPlugins/blob/experimental/src/main/java/com/impact/PowerGather/PowerGatherPlugin.java#L196 + //Big thanks hawkkkkkk + String[] tools = config.toolsToUse().split(","); // split the tools listed by comma, no space. + + int numInventoryTools = Inventory.search() + .filter(item -> isTool(item.getName())) // filter inventory by using out isTool method + .result().size(); + int numEquippedTools = Equipment.search() + .filter(item -> isTool(item.getName())) // filter inventory by using out isTool method + .result().size(); + + return numInventoryTools + numEquippedTools >= tools.length; // if the size of tools and the filtered inventory is the same, we have our tools. + } + + private void setTimeout() { + timeout = RandomUtils.nextInt(config.tickdelayMin(), config.tickDelayMax()); + } + + private boolean isTool(String name) { + String[] tools = config.toolsToUse().split(","); // split the tools listed by comma, no space. + + return Arrays.stream(tools) // stream the array using Arrays.stream() from java.util + .anyMatch(i -> name.toLowerCase().contains(i.toLowerCase())); // more likely for user error than the shouldKeep option, but we'll follow the same idea as shouldKeep. + } + + private final HotkeyListener toggle = new HotkeyListener(() -> config.toggle()) { + @Override + public void hotkeyPressed() { + toggle(); + } + }; + + private boolean shouldBank() { + return config.shouldBank() && + (NPCs.search().withAction("Bank").first().isPresent() || TileObjects.search().withAction("Bank").first().isPresent() + || TileObjects.search().withAction("Collect").first().isPresent() && !bankPin); + } + + public void toggle() { + if (client.getGameState() != GameState.LOGGED_IN) { + return; + } + started = !started; + } +} \ No newline at end of file diff --git a/PowerSkiller/src/main/java/com/example/State.java b/PowerSkiller/src/main/java/com/example/State.java new file mode 100644 index 00000000..e52e7d20 --- /dev/null +++ b/PowerSkiller/src/main/java/com/example/State.java @@ -0,0 +1,10 @@ +package com.example; + +public enum State { + FIND_OBJECT, + ANIMATING, + DROP_ITEMS, + MISSING_TOOLS, + BANK, + TIMEOUT +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0c10810f..75cd9531 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,8 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion + implementation 'com.ethanvann:packetutils:5.4' + implementation project(':PowerSkiller') } group = 'com.example' diff --git a/settings.gradle b/settings.gradle index b34111ec..9d14f90a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ rootProject.name = 'example' + +include 'PowerSkiller' \ No newline at end of file diff --git a/src/test/java/com/example/ExamplePluginTest.java b/src/test/java/com/example/ExamplePluginTest.java index 389198ee..460d6073 100644 --- a/src/test/java/com/example/ExamplePluginTest.java +++ b/src/test/java/com/example/ExamplePluginTest.java @@ -3,11 +3,13 @@ import net.runelite.client.RuneLite; import net.runelite.client.externalplugins.ExternalPluginManager; +import com.example.PowerSkillerPlugin; + public class ExamplePluginTest { public static void main(String[] args) throws Exception { - ExternalPluginManager.loadBuiltin(ExamplePlugin.class); + ExternalPluginManager.loadBuiltin(ExamplePlugin.class, PowerSkillerPlugin.class); RuneLite.main(args); } } \ No newline at end of file From da27b61abfe2445606dbd159accf3da5abc1d280 Mon Sep 17 00:00:00 2001 From: kasperknudas Date: Wed, 20 Aug 2025 14:47:57 +0200 Subject: [PATCH 2/3] Import of PowerSkillerPlugin and configured gradle files and dependencies accordingly --- PowerSkiller/build.gradle | 42 --------- build.gradle | 8 +- .../PowerSkiller}/PowerSkillerConfig.java | 8 +- .../PowerSkiller}/PowerSkillerOverlay.java | 2 +- .../PowerSkiller}/PowerSkillerPlugin.java | 88 ++++++++++++++----- .../java/com/example/PowerSkiller}/State.java | 2 +- .../java/com/example/ExamplePluginTest.java | 8 +- 7 files changed, 83 insertions(+), 75 deletions(-) delete mode 100644 PowerSkiller/build.gradle rename {PowerSkiller/src/main/java/com/example => src/main/java/com/example/PowerSkiller}/PowerSkillerConfig.java (96%) rename {PowerSkiller/src/main/java/com/example => src/main/java/com/example/PowerSkiller}/PowerSkillerOverlay.java (98%) rename {PowerSkiller/src/main/java/com/example => src/main/java/com/example/PowerSkiller}/PowerSkillerPlugin.java (80%) rename {PowerSkiller/src/main/java/com/example => src/main/java/com/example/PowerSkiller}/State.java (76%) diff --git a/PowerSkiller/build.gradle b/PowerSkiller/build.gradle deleted file mode 100644 index eb88c2d2..00000000 --- a/PowerSkiller/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - id 'java' -} - -group = 'com.example' -version = '1.0-SNAPSHOT' - - -repositories { - mavenLocal() // Important: so PacketUtils can be found - mavenCentral() - maven { - url = 'https://repo.runelite.net' - content { - includeGroupByRegex("net\\.runelite.*") - } - } -} - -def runeLiteVersion = '1.11.14-SNAPSHOT' // Match what you built and installed from the RuneLite repo - -dependencies { - // RuneLite core API - compileOnly "net.runelite:client:$runeLiteVersion" - compileOnly "net.runelite:runelite-api:$runeLiteVersion" - - // Your locally published EthanAPI / PacketUtils - implementation 'com.ethanvann:packetutils:5.4' - - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' -} - -test { - useJUnitPlatform() -} - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 75cd9531..28bf3801 100644 --- a/build.gradle +++ b/build.gradle @@ -24,10 +24,16 @@ dependencies { testImplementation 'junit:junit:4.12' testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion + implementation 'org.slf4j:slf4j-api:1.7.36' implementation 'com.ethanvann:packetutils:5.4' - implementation project(':PowerSkiller') + testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.2" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.2" + testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.10.2" } +tasks.test { + useJUnitPlatform() +} group = 'com.example' version = '1.0-SNAPSHOT' diff --git a/PowerSkiller/src/main/java/com/example/PowerSkillerConfig.java b/src/main/java/com/example/PowerSkiller/PowerSkillerConfig.java similarity index 96% rename from PowerSkiller/src/main/java/com/example/PowerSkillerConfig.java rename to src/main/java/com/example/PowerSkiller/PowerSkillerConfig.java index f9310154..e5a4b463 100644 --- a/PowerSkiller/src/main/java/com/example/PowerSkillerConfig.java +++ b/src/main/java/com/example/PowerSkiller/PowerSkillerConfig.java @@ -1,4 +1,4 @@ -package com.example; +package com.example.PowerSkiller; import net.runelite.client.config.*; @@ -53,7 +53,7 @@ default boolean searchNpc() { @ConfigItem( name = "Object", keyName = "objectToInteract", - description = "Game obejct you will be interacting with", + description = "Game object you will be interacting with", position = 0 ) default String objectToInteract() { @@ -62,7 +62,7 @@ default String objectToInteract() { @ConfigItem( name = "Expected Action", - keyName = "Expected Action", + keyName = "expectedAction", description = "The action you wish to do on the object", position = 1 ) @@ -98,7 +98,7 @@ default String itemsToKeep() { position = 4, section = tickDelaySection ) - default int tickdelayMin() { + default int tickDelayMin() { return 0; } diff --git a/PowerSkiller/src/main/java/com/example/PowerSkillerOverlay.java b/src/main/java/com/example/PowerSkiller/PowerSkillerOverlay.java similarity index 98% rename from PowerSkiller/src/main/java/com/example/PowerSkillerOverlay.java rename to src/main/java/com/example/PowerSkiller/PowerSkillerOverlay.java index 037f3498..f8fe5bf6 100644 --- a/PowerSkiller/src/main/java/com/example/PowerSkillerOverlay.java +++ b/src/main/java/com/example/PowerSkiller/PowerSkillerOverlay.java @@ -1,4 +1,4 @@ -package com.example; +package com.example.PowerSkiller; import com.google.inject.Inject; import net.runelite.client.ui.overlay.OverlayPanel; diff --git a/PowerSkiller/src/main/java/com/example/PowerSkillerPlugin.java b/src/main/java/com/example/PowerSkiller/PowerSkillerPlugin.java similarity index 80% rename from PowerSkiller/src/main/java/com/example/PowerSkillerPlugin.java rename to src/main/java/com/example/PowerSkiller/PowerSkillerPlugin.java index bffa445e..48554aa4 100644 --- a/PowerSkiller/src/main/java/com/example/PowerSkillerPlugin.java +++ b/src/main/java/com/example/PowerSkiller/PowerSkillerPlugin.java @@ -1,7 +1,6 @@ -package com.example; +package com.example.PowerSkiller; import com.example.EthanApiPlugin.Collections.*; -import com.example.EthanApiPlugin.Collections.query.TileObjectQuery; import com.example.EthanApiPlugin.EthanApiPlugin; import com.example.InteractionApi.BankInventoryInteraction; import com.example.InteractionApi.InventoryInteraction; @@ -9,6 +8,7 @@ import com.example.InteractionApi.TileObjectInteraction; import com.google.inject.Provides; import net.runelite.api.*; +import net.runelite.api.GameObject; import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.GameTick; import net.runelite.api.widgets.Widget; @@ -20,15 +20,16 @@ import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.util.HotkeyListener; +import lombok.extern.slf4j.Slf4j; import com.google.inject.Inject; import net.runelite.client.util.Text; import org.apache.commons.lang3.RandomUtils; import java.util.*; - +@Slf4j @PluginDescriptor( - name = "[PP] Power Skiller", + name = "[PP]PowerSkillerPlugin", description = "Will interact with an object and drop or bank all items when inventory is full", tags = {"ethan", "piggy", "skilling"} ) @@ -52,9 +53,11 @@ public class PowerSkillerPlugin extends Plugin { @Override protected void startUp() throws Exception { + log.info("startUp Powerskiller"); bankPin = false; keyManager.registerKeyListener(toggle); this.overlayManager.add(overlay); + log.info("Ending Startup Powerskiller"); } @Override @@ -69,13 +72,28 @@ private PowerSkillerConfig getConfig(ConfigManager configManager) { return configManager.getConfig(PowerSkillerConfig.class); } + private int debugEveryXTicks = 5; + private int tickCounter = 0; + @Subscribe private void onGameTick(GameTick event) { + + tickCounter++; + state = getNextState(); + if (!EthanApiPlugin.loggedIn() || !started) { // We do an early return if the user isn't logged in return; } state = getNextState(); + + + if (tickCounter % debugEveryXTicks == 0) { + log.info("[PS] state={}, invFull={}, anim={}, moving={}", + state, Inventory.full(), client.getLocalPlayer().getAnimation(), + EthanApiPlugin.isMoving()); + } + handleState(); } @@ -91,13 +109,13 @@ private void handleState() { TileObjectInteraction.interact(tileObject, "Bank"); return; }); - /* Outdated - check to fix later. + /* Outdated - check to fix later. NPCs.search().withAction("Bank").nearestToPlayer().ifPresent(npc -> { - if (EthanApiPlugin.pathToGoal(npc.getWorldLocation(), new HashSet<>()) != null) { + if (EthanApiPlugin.pathToGoalSet(npc.getWorldLocation(), new HashSet<>(), new HashSet<>(), new HashSet<>(), null) != null) { NPCInteraction.interact(npc, "Bank"); } return; - }); */ + }); */ TileObjects.search().withName("Bank chest").nearestToPlayer().ifPresent(tileObject -> { TileObjectInteraction.interact(tileObject, "Use"); return; @@ -124,7 +142,11 @@ private void handleState() { if (config.searchNpc()) { findNpc(); } else { - findObject(); + + GameObject gameObj = findObject(config.objectToInteract()); + if(gameObj != null) { + log.info("GameObject found: " + gameObj.getId()); + } } setTimeout(); break; @@ -165,21 +187,34 @@ private State getNextState() { // default it'll look for an object. return State.FIND_OBJECT; } - - private void findObject() { - String objectName = config.objectToInteract(); - if (config.useForestryTreeNotClosest() && config.expectedAction().equalsIgnoreCase("chop")) { - TileObjects.search().withName(objectName).nearestToPoint(getObjectWMostPlayers()).ifPresent(tileObject -> { - ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); - TileObjectInteraction.interact(tileObject, comp.getActions()[0]); - }); - } else { - TileObjects.search().withName(objectName).nearestToPlayer().ifPresent(tileObject -> { - ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); - TileObjectInteraction.interact(tileObject, comp.getActions()[0]); // find the object we're looking for. this specific example will only work if the first Action the object has is the one that interacts with it. - // don't *always* do this, you can manually type the possible actions. eg. "Mine", "Chop", "Cook", "Climb". - }); + // Implementation details below + // 1. search the runelite screen (tiles) for the GameObject input. + // 2. If found, return the location of the gameobject (I think) or the gameobject.. + // check if gameobjects are unique, i.e. the location can be extracted later + // Handle multiple matches, i.e. + // 3. if not, return null + + private GameObject findObject(String objectName) { + final Scene scene = client.getScene(); + + for (Tile[][] plane : scene.getTiles()) { + for (Tile[] row : plane) { + for (Tile tile : row) { + if (tile != null) { + for (GameObject obj : tile.getGameObjects()) { + if (obj != null) { + ObjectComposition def = client.getObjectDefinition(obj.getId()); + if (def.getName().equalsIgnoreCase(objectName)) { + log.info("object match! Name of object: "+ def.getName()); + return obj; + } + } + } + } + } + } } + return null; // Not found } /** @@ -215,8 +250,14 @@ public WorldPoint getObjectWMostPlayers() { private void findNpc() { String npcName = config.objectToInteract(); + log.info("NPCName:"+ npcName); + log.info("NPCs nearesttoPlayer: "+ NPCs.search().withName(npcName).nearestToPlayer()); + log.info("NPCs search: "+ NPCs.search().withName(npcName)); NPCs.search().withName(npcName).nearestToPlayer().ifPresent(npc -> { + log.info("NPC: "+ npc); NPCComposition comp = client.getNpcDefinition(npc.getId()); + log.info("comp NPC: "+ comp); + log.info("comp.getActions(): "+ comp.getActions()[0]); if (Arrays.stream(comp.getActions()).anyMatch(action -> action.equalsIgnoreCase(config.expectedAction()))) { NPCInteraction.interact(npc, config.expectedAction()); // For fishing spots ? } else { @@ -227,6 +268,7 @@ private void findNpc() { } private void dropItems() { + System.out.println("Inside dropItems"); List itemsToDrop = Inventory.search() .filter(item -> !shouldKeep(item.getName()) && !isTool(item.getName())).result(); // filter the inventory to only get the items we want to drop @@ -277,7 +319,7 @@ private boolean hasTools() { } private void setTimeout() { - timeout = RandomUtils.nextInt(config.tickdelayMin(), config.tickDelayMax()); + timeout = RandomUtils.nextInt(config.tickDelayMin(), config.tickDelayMax()); } private boolean isTool(String name) { diff --git a/PowerSkiller/src/main/java/com/example/State.java b/src/main/java/com/example/PowerSkiller/State.java similarity index 76% rename from PowerSkiller/src/main/java/com/example/State.java rename to src/main/java/com/example/PowerSkiller/State.java index e52e7d20..38df9d5b 100644 --- a/PowerSkiller/src/main/java/com/example/State.java +++ b/src/main/java/com/example/PowerSkiller/State.java @@ -1,4 +1,4 @@ -package com.example; +package com.example.PowerSkiller; public enum State { FIND_OBJECT, diff --git a/src/test/java/com/example/ExamplePluginTest.java b/src/test/java/com/example/ExamplePluginTest.java index 460d6073..fbfaa0c3 100644 --- a/src/test/java/com/example/ExamplePluginTest.java +++ b/src/test/java/com/example/ExamplePluginTest.java @@ -2,14 +2,16 @@ import net.runelite.client.RuneLite; import net.runelite.client.externalplugins.ExternalPluginManager; - -import com.example.PowerSkillerPlugin; +import com.example.PowerSkiller.PowerSkillerPlugin; +import com.example.PowerSkiller.PSDummy; +import com.example.EthanApiPlugin.EthanApiPlugin; +import com.example.PacketUtils.PacketUtilsPlugin; public class ExamplePluginTest { public static void main(String[] args) throws Exception { - ExternalPluginManager.loadBuiltin(ExamplePlugin.class, PowerSkillerPlugin.class); + ExternalPluginManager.loadBuiltin(PacketUtilsPlugin.class, EthanApiPlugin.class, PowerSkillerPlugin.class); RuneLite.main(args); } } \ No newline at end of file From 9f5570580aa66e56a3c9356ce538bb64eeac6dd9 Mon Sep 17 00:00:00 2001 From: kasperknudas Date: Fri, 7 Nov 2025 08:35:57 +0100 Subject: [PATCH 3/3] Some updates, code not working yet. --- build.gradle | 36 +- settings.gradle | 11 +- .../ChinBreakHandler/ChinBreakHandler.java | 371 ++++++++ .../ChinBreakHandlerPlugin.java | 881 ++++++++++++++++++ .../java/ChinBreakHandler/NullConfig.java | 8 + .../java/ChinBreakHandler/OptionsConfig.java | 150 +++ src/main/java/ChinBreakHandler/State.java | 21 + .../ui/ChinBreakHandlerAccountPanel.java | 300 ++++++ .../ui/ChinBreakHandlerPanel.java | 388 ++++++++ .../ui/ChinBreakHandlerPluginPanel.java | 315 +++++++ .../ui/ChinBreakHandlerStatusPanel.java | 419 +++++++++ .../java/ChinBreakHandler/ui/LoginMode.java | 19 + .../ChinBreakHandler/util/ConfigPanel.java | 449 +++++++++ .../util/DeferredDocumentChangedListener.java | 57 ++ .../util/FixedWidthPanel.java | 15 + .../util/IntRandomNumberGenerator.java | 31 + .../util/JMultilineLabel.java | 30 + .../util/MacOSPopupFactory.java | 13 + .../util/OnOffToggleButton.java | 39 + .../ChinBreakHandler/util/ProfilesData.java | 80 ++ .../util/SwingUtilExtended.java | 297 ++++++ .../ChinBreakHandler/util/UnitFormatter.java | 69 ++ .../util/UnitFormatterFactory.java | 44 + src/main/java/com/example/ExampleConfig.java | 19 - src/main/java/com/example/ExamplePlugin.java | 53 -- .../GemCrabFighter/GemCrabFighterConfig.java | 61 ++ .../GemCrabFighter/GemCrabFighterOverlay.java | 132 +++ .../GemCrabFighter/GemCrabFighterPlugin.java | 352 +++++++ .../GemCrabFighter/data/Constants.java | 95 ++ .../example/GemCrabFighter/data/State.java | 12 + .../example/GemCrabFighter/data/SubState.java | 29 + .../com/example/PiggyUtils/API/BankUtil.java | 78 ++ .../example/PiggyUtils/API/EquipmentUtil.java | 61 ++ .../example/PiggyUtils/API/InventoryUtil.java | 185 ++++ .../com/example/PiggyUtils/API/MathUtil.java | 28 + .../com/example/PiggyUtils/API/NpcUtil.java | 23 + .../example/PiggyUtils/API/ObjectUtil.java | 55 ++ .../example/PiggyUtils/API/PlayerUtil.java | 124 +++ .../example/PiggyUtils/API/PrayerUtil.java | 66 ++ .../com/example/PiggyUtils/API/Runes.java | 73 ++ .../com/example/PiggyUtils/API/SpellUtil.java | 20 + .../example/PiggyUtils/PiggyUtilsPlugin.java | 16 + .../PiggyUtils/RLApi/GraphicIDExtended.java | 96 ++ .../PowerSkiller/PowerSkillerConfig.java | 11 + .../PowerSkiller/PowerSkillerPlugin.java | 197 ++-- src/main/resources/chin_special.png | Bin 0 -> 1816 bytes src/main/resources/help.png | Bin 0 -> 404 bytes src/main/resources/switcher_on.png | Bin 0 -> 315 bytes .../java/com/example/ExamplePluginTest.java | 4 +- 49 files changed, 5678 insertions(+), 155 deletions(-) create mode 100644 src/main/java/ChinBreakHandler/ChinBreakHandler.java create mode 100644 src/main/java/ChinBreakHandler/ChinBreakHandlerPlugin.java create mode 100644 src/main/java/ChinBreakHandler/NullConfig.java create mode 100644 src/main/java/ChinBreakHandler/OptionsConfig.java create mode 100644 src/main/java/ChinBreakHandler/State.java create mode 100644 src/main/java/ChinBreakHandler/ui/ChinBreakHandlerAccountPanel.java create mode 100644 src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPanel.java create mode 100644 src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPluginPanel.java create mode 100644 src/main/java/ChinBreakHandler/ui/ChinBreakHandlerStatusPanel.java create mode 100644 src/main/java/ChinBreakHandler/ui/LoginMode.java create mode 100644 src/main/java/ChinBreakHandler/util/ConfigPanel.java create mode 100644 src/main/java/ChinBreakHandler/util/DeferredDocumentChangedListener.java create mode 100644 src/main/java/ChinBreakHandler/util/FixedWidthPanel.java create mode 100644 src/main/java/ChinBreakHandler/util/IntRandomNumberGenerator.java create mode 100644 src/main/java/ChinBreakHandler/util/JMultilineLabel.java create mode 100644 src/main/java/ChinBreakHandler/util/MacOSPopupFactory.java create mode 100644 src/main/java/ChinBreakHandler/util/OnOffToggleButton.java create mode 100644 src/main/java/ChinBreakHandler/util/ProfilesData.java create mode 100644 src/main/java/ChinBreakHandler/util/SwingUtilExtended.java create mode 100644 src/main/java/ChinBreakHandler/util/UnitFormatter.java create mode 100644 src/main/java/ChinBreakHandler/util/UnitFormatterFactory.java delete mode 100644 src/main/java/com/example/ExampleConfig.java delete mode 100644 src/main/java/com/example/ExamplePlugin.java create mode 100644 src/main/java/com/example/GemCrabFighter/GemCrabFighterConfig.java create mode 100644 src/main/java/com/example/GemCrabFighter/GemCrabFighterOverlay.java create mode 100644 src/main/java/com/example/GemCrabFighter/GemCrabFighterPlugin.java create mode 100644 src/main/java/com/example/GemCrabFighter/data/Constants.java create mode 100644 src/main/java/com/example/GemCrabFighter/data/State.java create mode 100644 src/main/java/com/example/GemCrabFighter/data/SubState.java create mode 100644 src/main/java/com/example/PiggyUtils/API/BankUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/EquipmentUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/InventoryUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/MathUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/NpcUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/ObjectUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/PlayerUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/PrayerUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/API/Runes.java create mode 100644 src/main/java/com/example/PiggyUtils/API/SpellUtil.java create mode 100644 src/main/java/com/example/PiggyUtils/PiggyUtilsPlugin.java create mode 100644 src/main/java/com/example/PiggyUtils/RLApi/GraphicIDExtended.java create mode 100644 src/main/resources/chin_special.png create mode 100644 src/main/resources/help.png create mode 100644 src/main/resources/switcher_on.png diff --git a/build.gradle b/build.gradle index 28bf3801..35939a8a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,34 +1,37 @@ plugins { id 'java' + id 'org.jetbrains.kotlin.jvm' } repositories { - mavenLocal() - maven { - url = 'https://repo.runelite.net' - content { - includeGroupByRegex("net\\.runelite.*") - } - } - mavenCentral() + mavenLocal() // Keep this first so it finds your local PacketUtils + maven { + url = 'https://repo.runelite.net' + content { includeGroupByRegex("net\\.runelite.*") } + } + mavenCentral() + // Remove the JitPack line since you're using local now } def runeLiteVersion = 'latest.release' dependencies { - compileOnly group: 'net.runelite', name:'client', version: runeLiteVersion + compileOnly group: 'net.runelite', name:'client', version: runeLiteVersion - compileOnly 'org.projectlombok:lombok:1.18.30' - annotationProcessor 'org.projectlombok:lombok:1.18.30' + compileOnly 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' - testImplementation 'junit:junit:4.12' - testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion - testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion + testImplementation 'junit:junit:4.12' + testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion + testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion implementation 'org.slf4j:slf4j-api:1.7.36' - implementation 'com.ethanvann:packetutils:5.4' + implementation 'io.reactivex.rxjava3:rxjava:3.1.8' + implementation 'com.ethanvann:packetutils:5.4-SNAPSHOT' + testImplementation "org.junit.jupiter:junit-jupiter-api:5.10.2" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.2" testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.10.2" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } tasks.test { @@ -67,3 +70,6 @@ tasks.register('shadowJar', Jar) { archiveClassifier.set('shadow') archiveFileName.set("${rootProject.name}-${project.version}-all.jar") } +kotlin { + jvmToolchain(11) +} diff --git a/settings.gradle b/settings.gradle index 9d14f90a..01bc982e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,12 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version '2.2.10' + } +} +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} rootProject.name = 'example' -include 'PowerSkiller' \ No newline at end of file +include 'PowerSkiller' +include 'ChinBreakHandler' \ No newline at end of file diff --git a/src/main/java/ChinBreakHandler/ChinBreakHandler.java b/src/main/java/ChinBreakHandler/ChinBreakHandler.java new file mode 100644 index 00000000..34c5ad24 --- /dev/null +++ b/src/main/java/ChinBreakHandler/ChinBreakHandler.java @@ -0,0 +1,371 @@ +package ChinBreakHandler; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.PublishSubject; +import net.runelite.api.Client; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.events.ConfigChanged; +import ChinBreakHandler.ui.LoginMode; +import ChinBreakHandler.util.IntRandomNumberGenerator; +import net.runelite.client.plugins.Plugin; +import org.apache.commons.lang3.tuple.Pair; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +@SuppressWarnings("unused") +@Singleton +public class ChinBreakHandler { + private final ConfigManager configManager; + + private final Map plugins = new TreeMap<>((o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName())); + private final PublishSubject> pluginsSubject = PublishSubject.create(); + + private final Set activePlugins = new HashSet<>(); + private final PublishSubject> activeSubject = PublishSubject.create(); + + private final Map plannedBreaks = new HashMap<>(); + private final PublishSubject> plannedBreaksSubject = PublishSubject.create(); + + private final Map activeBreaks = new HashMap<>(); + private final PublishSubject> activeBreaksSubject = PublishSubject.create(); + private final PublishSubject> currentActiveBreaksSubject = PublishSubject.create(); + + private final Map startTimes = new HashMap<>(); + private final Map amountOfBreaks = new HashMap<>(); + + private final PublishSubject logoutActionSubject = PublishSubject.create(); + private final PublishSubject loginActionSubject = PublishSubject.create(); + + public final PublishSubject configChanged = PublishSubject.create(); + + private final Map> extraData = new HashMap<>(); + private final PublishSubject>> extraDataSubject = PublishSubject.create(); + + @Inject + ChinBreakHandler(ConfigManager configManager) + { + this.configManager = configManager; + } + + public Map getPlugins() + { + return plugins; + } + + public void registerPlugin(Plugin plugin) + { + registerPlugin(plugin, true); + } + + public void registerPlugin(Plugin plugin, boolean configurable) + { + plugins.put(plugin, configurable); + pluginsSubject.onNext(plugins); + } + + public void unregisterPlugin(Plugin plugin) + { + plugins.remove(plugin); + pluginsSubject.onNext(plugins); + } + + public @NonNull Observable> getPluginObservable() + { + return pluginsSubject.hide(); + } + + public Set getActivePlugins() + { + return activePlugins; + } + + public void startPlugin(Plugin plugin) + { + activePlugins.add(plugin); + activeSubject.onNext(activePlugins); + + startTimes.put(plugin, Instant.now()); + amountOfBreaks.put(plugin, 0); + } + + public void stopPlugin(Plugin plugin) + { + activePlugins.remove(plugin); + activeSubject.onNext(activePlugins); + + removePlannedBreak(plugin); + stopBreak(plugin); + + startTimes.remove(plugin); + amountOfBreaks.remove(plugin); + } + + public @NonNull Observable> getActiveObservable() + { + return activeSubject.hide(); + } + + public Map getPlannedBreaks() + { + return plannedBreaks; + } + + public void planBreak(Plugin plugin, Instant instant) + { + plannedBreaks.put(plugin, instant); + plannedBreaksSubject.onNext(plannedBreaks); + } + + public void removePlannedBreak(Plugin plugin) + { + plannedBreaks.remove(plugin); + plannedBreaksSubject.onNext(plannedBreaks); + } + + public @NonNull Observable> getPlannedBreaksObservable() + { + return plannedBreaksSubject.hide(); + } + + public boolean isBreakPlanned(Plugin plugin) + { + return plannedBreaks.containsKey(plugin); + } + + public Instant getPlannedBreak(Plugin plugin) + { + return plannedBreaks.get(plugin); + } + + public boolean shouldBreak(Plugin plugin) + { + if (!plannedBreaks.containsKey(plugin)) + { + return false; + } + + return Instant.now().isAfter(getPlannedBreak(plugin)); + } + + public Map getActiveBreaks() + { + return activeBreaks; + } + + public static boolean needsBankPin(Client client) { + Widget w = client.getWidget(WidgetInfo.BANK_PIN_CONTAINER); + return w != null && !w.isHidden(); + } + + public static String getBankPin(ConfigManager configManager) { + LoginMode loginMode = LoginMode.parse(configManager.getConfiguration("chinBreakHandler", "accountselection")); + if (loginMode == null) { + return null; + } + + if (loginMode == LoginMode.PROFILES) { + String account = configManager.getConfiguration("chinBreakHandler", "accountselection-profiles-account"); + + if (ChinBreakHandlerPlugin.data == null) { + return null; + } + + Optional accountData = Arrays.stream(ChinBreakHandlerPlugin.data.split("\\n")) + .filter(s -> s.startsWith(account)) + .findFirst(); + + if (accountData.isPresent()) + { + String[] parts = accountData.get().split(":"); + if (parts.length == 4) + { + return parts[3]; + } else { + return parts[2]; + } + } + + return null; + } + + String pin = configManager.getConfiguration("chinBreakHandler", "accountselection-manual-pin"); + if (pin == null || pin.length() != 4) { + return null; + } + return pin; + } + + public static int getOrDefaultFrom(Plugin plugin, ConfigManager configManager) { + String s = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-breakfrom"); + if (s == null || s.isEmpty()) { + return 60 * 60; + } + return Integer.parseInt(s) * 60; + } + public static int getOrDefaultTo(Plugin plugin, ConfigManager configManager) { + String s = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-breakto"); + if (s == null || s.isEmpty()) { + return 60 * 60; + } + return Integer.parseInt(s) * 60; + } + + public void startBreak(Plugin plugin) + { + int from = getOrDefaultFrom(plugin, configManager); + int to = getOrDefaultTo(plugin, configManager); + + int random = new IntRandomNumberGenerator(from, to).nextInt(); + + removePlannedBreak(plugin); + + Instant breakUntil = Instant.now().plus(random, ChronoUnit.SECONDS); + + activeBreaks.put(plugin, breakUntil); + activeBreaksSubject.onNext(activeBreaks); + + currentActiveBreaksSubject.onNext(Pair.of(plugin, breakUntil)); + + if (amountOfBreaks.containsKey(plugin)) + { + amountOfBreaks.put(plugin, amountOfBreaks.get(plugin) + 1); + } + else + { + amountOfBreaks.put(plugin, 1); + } + } + + public void startBreak(Plugin plugin, Instant instant) + { + removePlannedBreak(plugin); + + activeBreaks.put(plugin, instant); + activeBreaksSubject.onNext(activeBreaks); + + currentActiveBreaksSubject.onNext(Pair.of(plugin, instant)); + + if (amountOfBreaks.containsKey(plugin)) + { + amountOfBreaks.put(plugin, amountOfBreaks.get(plugin) + 1); + } + else + { + amountOfBreaks.put(plugin, 1); + } + } + + public void stopBreak(Plugin plugin) + { + activeBreaks.remove(plugin); + activeBreaksSubject.onNext(activeBreaks); + } + + public void setExtraData(Plugin plugin, String key, String value) + { + extraData.putIfAbsent(plugin, new LinkedHashMap<>()); + extraData.get(plugin).put(key, value); + + extraDataSubject.onNext(extraData); + } + + public void setExtraData(Plugin plugin, Map data) + { + extraData.putIfAbsent(plugin, new LinkedHashMap<>()); + + data.forEach( + (key, value) -> extraData.get(plugin).merge(key, value, (existingData, newData) -> newData) + ); + + extraDataSubject.onNext(extraData); + } + + public void removeExtraData(Plugin plugin, String key) + { + if (!extraData.containsKey(plugin)) + { + return; + } + + extraData.get(plugin).remove(key); + extraDataSubject.onNext(extraData); + } + + public void resetExtraData(Plugin plugin) + { + extraData.remove(plugin); + extraDataSubject.onNext(extraData); + } + + public @NonNull Observable>> getExtraDataObservable() + { + return extraDataSubject.hide(); + } + + public @NonNull Observable> getActiveBreaksObservable() + { + return activeBreaksSubject.hide(); + } + + public @NonNull Observable> getCurrentActiveBreaksObservable() + { + return currentActiveBreaksSubject.hide(); + } + + public boolean isBreakActive(Plugin plugin) + { + return activeBreaks.containsKey(plugin); + } + + public Instant getActiveBreak(Plugin plugin) + { + return activeBreaks.get(plugin); + } + + public void logoutNow(Plugin plugin) + { + logoutActionSubject.onNext(plugin); + } + + public @NonNull Observable getlogoutActionObservable() + { + return logoutActionSubject.hide(); + } + + public @NonNull Observable getLoginActionObservable() { + return loginActionSubject.hide(); + } + + public Map getStartTimes() + { + return startTimes; + } + + public Map getAmountOfBreaks() + { + return amountOfBreaks; + } + + public int getTotalAmountOfBreaks() + { + return amountOfBreaks.values().stream().mapToInt(Integer::intValue).sum(); + } + + public void loginNow(Plugin plugin) { + loginActionSubject.onNext(plugin); + } +} diff --git a/src/main/java/ChinBreakHandler/ChinBreakHandlerPlugin.java b/src/main/java/ChinBreakHandler/ChinBreakHandlerPlugin.java new file mode 100644 index 00000000..c343cc7e --- /dev/null +++ b/src/main/java/ChinBreakHandler/ChinBreakHandlerPlugin.java @@ -0,0 +1,881 @@ +package ChinBreakHandler; + +import com.example.EthanApiPlugin.Collections.Widgets; +import com.example.PacketUtils.WidgetID; +import com.example.Packets.MousePackets; +import com.example.Packets.WidgetPackets; +import com.google.inject.Inject; +import com.google.inject.Provides; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.MenuAction; +import net.runelite.api.Point; +import net.runelite.api.VarClientInt; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.MenuOptionClicked; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.chat.ChatColorType; +import net.runelite.client.chat.ChatMessageBuilder; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.game.WorldService; +import ChinBreakHandler.ui.ChinBreakHandlerPanel; +import ChinBreakHandler.ui.LoginMode; +import ChinBreakHandler.util.IntRandomNumberGenerator; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.WorldUtil; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldResult; +import net.runelite.http.api.worlds.WorldType; +import org.apache.commons.lang3.tuple.Pair; + +import javax.swing.*; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + + +@PluginDescriptor( + name = "Chin break handler", + description = "Automatically takes breaks for you", + tags = {"ethan", "piggy", "break", "chin"} +) +@Slf4j +public class ChinBreakHandlerPlugin extends Plugin { + + private static final int DISPLAY_SWITCHER_MAX_ATTEMPTS = 3; + private static final int MAX_WORLD = 580; + + @Inject + private Client client; + + @Inject + private ClientToolbar clientToolbar; + + @Inject + private ClientThread clientThread; + + @Inject + @Getter + private ConfigManager configManager; + + @Inject + private ChinBreakHandler chinBreakHandler; + + @Inject + private OptionsConfig optionsConfig; + + @Inject + private WorldService worldService; + + @Inject + private ChatMessageManager chatMessageManager; + + @Provides + public NullConfig getConfig() { + return configManager.getConfig(NullConfig.class); + } + + @Provides + public OptionsConfig getOptionsConfig() { + return configManager.getConfig(OptionsConfig.class); + } + + public static String data; + + private NavigationButton navButton; + private ChinBreakHandlerPanel panel; + private boolean logout; + private int delay = -1; + private static final int[] F2P_WORLDS = { + 301, 308, 316, 326, 335, 371, 382, 383, 384, 393, 394, 395, 396, 397, 398, 399 + }; + + public final Map disposables = new HashMap<>(); + public Disposable activeBreaks; + public Disposable secondsDisposable; + public Disposable activeDisposable; + public Disposable logoutDisposable; + public Disposable loginDisposable; + + private State state = State.NULL; + private ExecutorService executorService; + + private net.runelite.api.World quickHopTargetWorld; + private int displaySwitcherAttempts = 0; + private boolean login = false; + + private int currentPinNumber = 0; + + protected void startUp() { + log.info("Chin break handler started!"); + executorService = Executors.newSingleThreadExecutor(); + + panel = injector.getInstance(ChinBreakHandlerPanel.class); + + final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "/chin_special.png"); + + Observable.interval(1, TimeUnit.SECONDS) + .subscribe(tick -> { + for (Plugin plugin : new HashSet<>(chinBreakHandler.getPlannedBreaks().keySet())) { + if (chinBreakHandler.shouldBreak(plugin)) { + log.info("Activating break for plugin: {}", plugin.getName()); + chinBreakHandler.startBreak(plugin); + } + } + }); + + activeBreaks = chinBreakHandler + .getCurrentActiveBreaksObservable() + .subscribe(this::breakActivated); + + + navButton = NavigationButton.builder() + .tooltip("Chin break handler") + .icon(icon) + .priority(4) + .panel(panel) + .build(); + clientToolbar.addNavigation(navButton); + + activeBreaks = chinBreakHandler + .getCurrentActiveBreaksObservable() + .subscribe(this::breakActivated); + + secondsDisposable = Observable + .interval(1, TimeUnit.SECONDS) + .subscribe(this::seconds); + + activeDisposable = chinBreakHandler + .getActiveObservable() + .subscribe( + (plugins) -> + { + if (!plugins.isEmpty()) { + if (!navButton.getPanel().isVisible()) { + clientToolbar.openPanel(navButton); + } + } + } + ); + + logoutDisposable = chinBreakHandler + .getlogoutActionObservable() + .subscribe( + (plugin) -> + { + if (plugin != null) { + if (state != State.LOGOUT && state != State.LOGOUT_TAB && state != State.LOGOUT_BUTTON && state != State.LOGOUT_WAIT) { + logout = true; + state = State.LOGOUT; + } + } + } + ); + + loginDisposable = chinBreakHandler + .getLoginActionObservable() + .subscribe( + (plugin -> { + if (plugin != null) { + login = true; + } + }) + ); + } + + protected void shutDown() { + executorService.shutdown(); + + clientToolbar.removeNavigation(navButton); + + panel.pluginDisposable.dispose(); + panel.activeDisposable.dispose(); + panel.currentDisposable.dispose(); + panel.startDisposable.dispose(); + panel.configDisposable.dispose(); + + for (Disposable disposable : disposables.values()) { + if (!disposable.isDisposed()) { + disposable.dispose(); + } + } + + if (activeBreaks != null && !activeBreaks.isDisposed()) { + activeBreaks.dispose(); + } + + if (secondsDisposable != null && !secondsDisposable.isDisposed()) { + secondsDisposable.dispose(); + } + + if (activeDisposable != null && !activeDisposable.isDisposed()) { + activeDisposable.dispose(); + } + + if (logoutDisposable != null && !logoutDisposable.isDisposed()) { + logoutDisposable.dispose(); + } + } + + @Subscribe + public void onConfigChanged(ConfigChanged configChanged) { + chinBreakHandler.configChanged.onNext(configChanged); + } + + public void scheduleBreak(Plugin plugin) { + log.debug("scheduleBreak called for plugin='{}'", plugin == null ? "null" : plugin.getName()); + String fromStr = configManager.getConfiguration("chinBreakHandler", sanitizedName(plugin) + "-thresholdfrom"); + String toStr = configManager.getConfiguration("chinBreakHandler", sanitizedName(plugin) + "-thresholdto"); + log.debug("scheduleBreak config: thresholdfrom='{}' thresholdto='{}'", fromStr, toStr); + + int from = Integer.parseInt(fromStr) * 60; + int to = Integer.parseInt(toStr) * 60; + + int random = new IntRandomNumberGenerator(from, to).nextInt(); + + Instant planned = Instant.now().plus(random, ChronoUnit.SECONDS); + log.debug("planning break for plugin='{}' at {}", plugin.getName(), planned); + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.CONSOLE) + .runeLiteFormattedMessage("ChinBreakHandler: planning break for " + plugin.getName() + " at " + planned) + .build()); + + chinBreakHandler.planBreak(plugin, planned); + } + + + private void breakActivated(Pair pluginInstantPair) { + Plugin plugin = pluginInstantPair.getKey(); + Instant when = pluginInstantPair.getValue(); + + // Log + console output for debugging + log.debug("breakActivated called for plugin='{}' at {}", plugin == null ? "null" : plugin.getName(), when); + chatMessageManager.queue(QueuedMessage.builder() + .type(ChatMessageType.CONSOLE) + .runeLiteFormattedMessage("ChinBreakHandler: breakActivated for " + (plugin == null ? "null" : plugin.getName())) + .build()); + + // dump plugin state to logs + dumpPluginConfigAndState(plugin); + + if (!chinBreakHandler.getPlugins().get(plugin) || Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", sanitizedName(plugin) + "-logout"))) { + logout = true; + state = State.LOGOUT; + } + } + + private void dumpPluginConfigAndState(Plugin plugin) { + if (plugin == null) { + log.debug("dumpPluginConfigAndState: plugin is null"); + return; + } + String name = sanitizedName(plugin); + log.debug("dumpPluginConfigAndState for plugin name='{}' sanitized='{}'", plugin.getName(), name); + + String thresholdFrom = configManager.getConfiguration("chinBreakHandler", name + "-thresholdfrom"); + String thresholdTo = configManager.getConfiguration("chinBreakHandler", name + "-thresholdto"); + String breakFrom = configManager.getConfiguration("chinBreakHandler", name + "-breakfrom"); + String breakTo = configManager.getConfiguration("chinBreakHandler", name + "-breakto"); + String logoutCfg = configManager.getConfiguration("chinBreakHandler", name + "-logout"); + + log.debug("config thresholdFrom='{}' thresholdTo='{}' breakFrom='{}' breakTo='{}' logout='{}'", thresholdFrom, thresholdTo, breakFrom, breakTo, logoutCfg); + + Map pluginsMap = chinBreakHandler.getPlugins(); + log.debug("chinBreakHandler.getPlugins() size={} containsKey={}", pluginsMap.size(), pluginsMap.containsKey(plugin)); + log.debug("pluginsMap.get(plugin) = {}", pluginsMap.get(plugin)); + } + + private boolean loginCheck() { + Map activeBreaks = chinBreakHandler.getActiveBreaks(); + Map plannedBreaks = chinBreakHandler.getPlannedBreaks(); + + if (login) { + return false; + } + + if (optionsConfig.autoLoginOnDisconnect()) { + return (activeBreaks.isEmpty() && plannedBreaks.isEmpty()); + } else { + return activeBreaks.isEmpty(); + } + } + + private void seconds(long ignored) { + Map activeBreaks = chinBreakHandler.getActiveBreaks(); + + if (loginCheck() || client.getGameState() != GameState.LOGIN_SCREEN) { + return; + } + + boolean finished = true; + + for (Instant duration : activeBreaks.values()) { + if (Instant.now().isBefore(duration)) { + finished = false; + } + } + + if (finished) { + LoginMode loginMode = LoginMode.parse(configManager.getConfiguration("chinBreakHandler", "accountselection")); + + String username = null; + String password = null; + + if (loginMode.equals(LoginMode.MANUAL)) { + username = configManager.getConfiguration("chinBreakHandler", "accountselection-manual-username"); + password = configManager.getConfiguration("chinBreakHandler", "accountselection-manual-password"); + } else if (loginMode.equals(LoginMode.PROFILES)) { + String account = configManager.getConfiguration("chinBreakHandler", "accountselection-profiles-account"); + + if (data == null) { + return; + } + + Optional accountData = Arrays.stream(data.split("\\n")) + .filter(s -> s.startsWith(account)) + .findFirst(); + + if (accountData.isPresent()) { + String[] parts = accountData.get().split(":"); + log.info("Account parts: " + Arrays.toString(parts)); + username = parts[1]; + if (parts.length == 4) { + password = parts[2]; + } + } + } else if (loginMode.equals(LoginMode.LAUNCHER)) { + clientThread.invoke(() -> { + log.info("Using launcher"); + // this might not even work tbh +// sendKey(KeyEvent.VK_ENTER); // do we need these? surely not +// sendKey(KeyEvent.VK_ENTER); +// sendKey(KeyEvent.VK_ENTER); + client.setGameState(GameState.LOGGING_IN); + }); + return; + } + + if (username != null && password != null) { + String finalUsername = username; + String finalPassword = password; + + clientThread.invoke(() -> + { + client.setUsername(finalUsername); + client.setPassword(finalPassword); + + sendKey(KeyEvent.VK_ENTER); + sendKey(KeyEvent.VK_ENTER); + sendKey(KeyEvent.VK_ENTER); + client.setGameState(GameState.LOGGING_IN); + } + ); + + } + } + } + + public static String sanitizedName(Plugin plugin) { + return plugin.getName().toLowerCase().replace(" ", ""); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged gameStateChanged) { + if (gameStateChanged.getGameState() == GameState.LOGIN_SCREEN || gameStateChanged.getGameState() == GameState.CONNECTION_LOST) { + state = State.LOGIN_SCREEN; + + if (optionsConfig.useF2PWorlds()) { + int currentWorld = client.getWorld(); + log.info("Current world: " + currentWorld); + if (!Arrays.stream(F2P_WORLDS).anyMatch(i -> i == currentWorld)) { + hop(F2P_WORLDS[new Random().nextInt(F2P_WORLDS.length)]); + return; + } + if (optionsConfig.hopAfterBreak() && (optionsConfig.american() + || optionsConfig.unitedKingdom() + || optionsConfig.german() + || optionsConfig.australian())) { + hop(); + } + } + + if (optionsConfig.stopAfterBreaks() != 0 && chinBreakHandler.getTotalAmountOfBreaks() >= optionsConfig.stopAfterBreaks()) { + for (Plugin plugin : Set.copyOf(chinBreakHandler.getActivePlugins())) { + chinBreakHandler.stopPlugin(plugin); + } + } + } + } + + public void typeString(char c) { + pressShitKey(c); + } + + public void pressShitKey(char key) { + skeyEvent(401, key); + skeyEvent(402, key); + skeyEvent(400, key); + } + + private void skeyEvent(int id, char key) { + KeyEvent e = new KeyEvent(client.getCanvas(), id, System.currentTimeMillis(), 0, KeyEvent.VK_UNDEFINED, key); + client.getCanvas().dispatchEvent(e); + } + + @Subscribe + public void onGameTick(GameTick gameTick) { + + if (client.getGameState() == GameState.LOGGED_IN && optionsConfig.autoBankPin()) { + Widget bankPinWidget = client.getWidget(213, 0); + if (bankPinWidget != null && !bankPinWidget.isHidden()) { + String pin = ChinBreakHandler.getBankPin(configManager); + if (pin != null && pin.length() == 4) { + typeString(pin.charAt(currentPinNumber)); + if (currentPinNumber < 3) { + currentPinNumber++; + } else { + currentPinNumber = 0; + } + client.setVarcIntValue(VarClientInt.BLOCK_KEYPRESS, client.getGameCycle() + 1); + } + } else if (bankPinWidget == null && currentPinNumber != 0) { + currentPinNumber = 0; + } + } + + if (state == State.NULL && logout && delay == 0) { + state = State.LOGOUT; + } else if (state == State.LOGIN_SCREEN && (!chinBreakHandler.getActiveBreaks().isEmpty() || login)) { + + MousePackets.queueClickPacket(); + WidgetPackets.queueWidgetActionPacket(1, 24772680, -1, -1); + logout = false; + + Widget loginScreen = client.getWidget(WidgetInfo.LOGIN_CLICK_TO_PLAY_SCREEN); + Widget playButtonText = client.getWidget(WidgetID.LOGIN_CLICK_TO_PLAY_GROUP_ID, 87); + + + if (playButtonText != null && playButtonText.getText().equals("CLICK HERE TO PLAY")) { + click(playButtonText); + } else if (loginScreen == null) { + state = State.INVENTORY; + } + } else if (state == State.LOGOUT) { + sendKey(KeyEvent.VK_ESCAPE); + + state = State.LOGOUT_TAB; + } else if (state == State.LOGOUT_TAB) { + // Logout tab + if (client.getVar(VarClientInt.INVENTORY_TAB) != 10) { + client.runScript(915, 10); + } + + Widget logoutButton = client.getWidget(182, 8); + Widget logoutDoorButton = client.getWidget(69, 23); + Optional widget = Widgets.search().withId(4522009).first(); + if (widget.isPresent()) { + MousePackets.queueClickPacket(); + WidgetPackets.queueWidgetActionPacket(1, 4522009, -1, -1); + } else { + MousePackets.queueClickPacket(); + WidgetPackets.queueWidgetActionPacket(1, 11927560, -1, -1); + } + + if (logoutButton != null || logoutDoorButton != null) { + state = State.LOGOUT_BUTTON; + } + } else if (state == State.LOGOUT_BUTTON) { + Widget logoutButton = client.getWidget(182, 8); + click(logoutButton); + delay = new IntRandomNumberGenerator(20, 25).nextInt(); + } else if (state == State.INVENTORY) { + // Inventory + if (client.getVar(VarClientInt.INVENTORY_TAB) != 3) { + client.runScript(915, 3); + } + state = State.RESUME; + } else if (state == State.RESUME) { + for (Plugin plugin : chinBreakHandler.getActiveBreaks().keySet()) { + chinBreakHandler.stopBreak(plugin); + } + + state = State.NULL; + } else if (!chinBreakHandler.getActiveBreaks().isEmpty()) { + Map activeBreaks = chinBreakHandler.getActiveBreaks(); + + if (activeBreaks + .keySet() + .stream() + .anyMatch(e -> + !Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", sanitizedName(e) + "-logout")))) { + if (client.getKeyboardIdleTicks() > 14900) { + KeyEvent keyEvent = new KeyEvent( + client.getCanvas(), KeyEvent.KEY_TYPED, System.currentTimeMillis(), + 0, KeyEvent.VK_0, KeyEvent.CHAR_UNDEFINED + ); + client.getCanvas().dispatchEvent(keyEvent); + } + if (client.getMouseIdleTicks() > 14900) { + Point point = new Point(0, 0); + MouseEvent mouseEvent = new MouseEvent( + client.getCanvas(), MouseEvent.MOUSE_MOVED, + System.currentTimeMillis(), + 0, point.getX(), point.getY(), + 0, false, 0 + ); + client.getCanvas().dispatchEvent(mouseEvent); + } + + boolean finished = true; + + for (Instant duration : activeBreaks.values()) { + if (Instant.now().isBefore(duration)) { + finished = false; + } + } + + if (finished) { + state = State.INVENTORY; + } + } + } + + if (delay > 0) { + delay--; + } + + if (quickHopTargetWorld == null) { + return; + } + + if (client.getWidget(WidgetInfo.WORLD_SWITCHER_LIST) == null) { + client.openWorldHopper(); + + if (++displaySwitcherAttempts >= DISPLAY_SWITCHER_MAX_ATTEMPTS) { + String chatMessage = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Failed to quick-hop after ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(displaySwitcherAttempts)) + .append(ChatColorType.NORMAL) + .append(" attempts.") + .build(); + + chatMessageManager + .queue(QueuedMessage.builder() + .type(ChatMessageType.CONSOLE) + .runeLiteFormattedMessage(chatMessage) + .build()); + + resetQuickHopper(); + } + } else { + client.hopToWorld(quickHopTargetWorld); + resetQuickHopper(); + } + } + + private void resetQuickHopper() { + displaySwitcherAttempts = 0; + quickHopTargetWorld = null; + } + + @Subscribe + public void onMenuOptionClicked(MenuOptionClicked menuOptionClicked) { + if (state == State.LOGIN_SCREEN) { + Widget playButton = client.getWidget(WidgetID.LOGIN_CLICK_TO_PLAY_GROUP_ID, 78); + + if (playButton == null) { + return; + } + + menuAction( + menuOptionClicked, + "Play", + "", + 1, + MenuAction.CC_OP, + -1, + playButton.getId() + ); + + state = State.INVENTORY; + } else if (state == State.LOGOUT_BUTTON) { + Widget logoutButton = client.getWidget(182, 8); + Widget logoutDoorButton = client.getWidget(69, 23); + int param1 = -1; + + if (logoutButton != null) { + param1 = logoutButton.getId(); + } else if (logoutDoorButton != null) { + param1 = logoutDoorButton.getId(); + } + + if (param1 == -1) { + menuOptionClicked.consume(); + return; + } + + menuAction( + menuOptionClicked, + "Logout", + "", + 1, + MenuAction.CC_OP, + -1, + param1 + ); + + state = State.NULL; + } + } + + private void click(Widget widget) { + MousePackets.queueClickPacket(); + WidgetPackets.queueWidgetAction(widget, widget.getActions()[0]); + } + + private void click() { + executorService.submit(() -> + { + Point point = new Point(0, 0); + + mouseEvent(MouseEvent.MOUSE_ENTERED, point); + mouseEvent(MouseEvent.MOUSE_EXITED, point); + mouseEvent(MouseEvent.MOUSE_MOVED, point); + + mouseEvent(MouseEvent.MOUSE_PRESSED, point); + mouseEvent(MouseEvent.MOUSE_RELEASED, point); + mouseEvent(MouseEvent.MOUSE_CLICKED, point); + }); + } + + private void mouseEvent(int id, Point point) { + MouseEvent mouseEvent = new MouseEvent( + client.getCanvas(), id, + System.currentTimeMillis(), + 0, point.getX(), point.getY(), + 1, false, 1 + ); + + client.getCanvas().dispatchEvent(mouseEvent); + } + + @SuppressWarnings("SameParameterValue") + private void sendKey(int key) { + keyEvent(KeyEvent.KEY_PRESSED, key); + keyEvent(KeyEvent.KEY_RELEASED, key); + } + + private void keyEvent(int id, int key) { + KeyEvent e = new KeyEvent( + client.getCanvas(), id, System.currentTimeMillis(), + 0, key, KeyEvent.CHAR_UNDEFINED + ); + + client.getCanvas().dispatchEvent(e); + } + + public boolean isValidBreak(Plugin plugin) { + Map plugins = chinBreakHandler.getPlugins(); + + if (!plugins.containsKey(plugin)) { + return false; + } + + if (!plugins.get(plugin)) { + return true; + } + + String thresholdfrom = configManager.getConfiguration("chinBreakHandler", sanitizedName(plugin) + "-thresholdfrom"); + String thresholdto = configManager.getConfiguration("chinBreakHandler", sanitizedName(plugin) + "-thresholdto"); + String breakfrom = configManager.getConfiguration("chinBreakHandler", sanitizedName(plugin) + "-breakfrom"); + String breakto = configManager.getConfiguration("chinBreakHandler", sanitizedName(plugin) + "-breakto"); + + return isNumeric(thresholdfrom) && + isNumeric(thresholdto) && + isNumeric(breakfrom) && + isNumeric(breakto) && + Integer.parseInt(thresholdfrom) <= Integer.parseInt(thresholdto) && + Integer.parseInt(breakfrom) <= Integer.parseInt(breakto); + } + + public static boolean isNumeric(String strNum) { + if (strNum == null) { + return false; + } + try { + Double.parseDouble(strNum); + } catch (NumberFormatException nfe) { + return false; + } + return true; + } + + public void menuAction(MenuOptionClicked menuOptionClicked, String option, String target, int identifier, MenuAction menuAction, int param0, int param1) { +// menuOptionClicked.setMenuOption(option); +// menuOptionClicked.setMenuTarget(target); +// menuOptionClicked.setId(identifier); +// menuOptionClicked.setMenuAction(menuAction); +// menuOptionClicked.setActionParam(param0); +// menuOptionClicked.setWidgetId(param1); + } + + private World findWorld(List worlds, EnumSet currentWorldTypes, int totalLevel) { + World world = worlds.get(new Random().nextInt(worlds.size())); + + EnumSet types = world.getTypes().clone(); + + types.remove(WorldType.LAST_MAN_STANDING); + + if (types.contains(WorldType.SKILL_TOTAL)) { + try { + int totalRequirement = Integer.parseInt(world.getActivity().substring(0, world.getActivity().indexOf(" "))); + + if (totalLevel >= totalRequirement) { + types.remove(WorldType.SKILL_TOTAL); + } + } catch (NumberFormatException ex) { + log.warn("Failed to parse total level requirement for target world", ex); + } + } + + if (currentWorldTypes.equals(types)) { + int worldLocation = world.getLocation(); + + if (Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", "american")) && worldLocation == 0) { + return world; + } else if (Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", "united-kingdom")) && worldLocation == 1) { + return world; + } else if (Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", "australian")) && worldLocation == 3) { + return world; + } else if (Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", "german")) && worldLocation == 7) { + return world; + } + } + + return null; + } + + private void hop() { + clientThread.invoke(() -> { + WorldResult worldResult = worldService.getWorlds(); + log.info("WorldResult in Hop(): " + worldResult); + if (worldResult == null) { + return; + } + + World currentWorld = worldResult.findWorld(client.getWorld()); + + if (currentWorld == null) { + return; + } + + EnumSet currentWorldTypes = currentWorld.getTypes().clone(); + + currentWorldTypes.remove(WorldType.PVP); + currentWorldTypes.remove(WorldType.HIGH_RISK); + currentWorldTypes.remove(WorldType.BOUNTY); + currentWorldTypes.remove(WorldType.SKILL_TOTAL); + currentWorldTypes.remove(WorldType.LAST_MAN_STANDING); + + List worlds = worldResult.getWorlds(); + + String[] badWorldIds = optionsConfig.avoidWorldsNumbers().replace(", ", ",").split(","); + List badWorlds = new ArrayList<>(); + for (String worldIdString : badWorldIds) { + if (!isNumeric(worldIdString)) { + continue; + } else { + int id = Integer.valueOf(worldIdString); + if (id <= MAX_WORLD) { + badWorlds.add(id); + } + } + } + + worlds.removeIf(world -> world.getPlayers() >= optionsConfig.avoidWorldsPlayerCount() || badWorlds.contains(world.getId())); + + int totalLevel = client.getTotalLevel(); + + World world; + do { + world = findWorld(worlds, currentWorldTypes, totalLevel); + } + while (world == null || world == currentWorld); + + hop(world.getId()); + }); + } + + private void hop(int worldId) { + log.info("worldId: " + worldId); + WorldResult worldResult = worldService.getWorlds(); + // Don't try to hop if the world doesn't exist + World world = worldResult.findWorld(worldId); + log.info( + "WorldResult in Hop(int worldId): " + worldResult + " | World in Hop(int worldId): " + world + ); + if (world == null) { + return; + } + + final net.runelite.api.World rsWorld = client.createWorld(); + rsWorld.setActivity(world.getActivity()); + rsWorld.setAddress(world.getAddress()); + rsWorld.setId(world.getId()); + rsWorld.setPlayerCount(world.getPlayers()); + rsWorld.setLocation(world.getLocation()); + rsWorld.setTypes(WorldUtil.toWorldTypes(world.getTypes())); + + if (client.getGameState() == GameState.LOGIN_SCREEN) { + client.changeWorld(rsWorld); + return; + } + + String chatMessage = new ChatMessageBuilder() + .append(ChatColorType.NORMAL) + .append("Hopping away from a player. New world: ") + .append(ChatColorType.HIGHLIGHT) + .append(Integer.toString(world.getId())) + .append(ChatColorType.NORMAL) + .append("..") + .build(); + + chatMessageManager + .queue(QueuedMessage.builder() + .type(ChatMessageType.CONSOLE) + .runeLiteFormattedMessage(chatMessage) + .build()); + + quickHopTargetWorld = rsWorld; + displaySwitcherAttempts = 0; + } + + public JFrame getFrame() { + return (JFrame) SwingUtilities.getRoot(client.getCanvas()); + } +} diff --git a/src/main/java/ChinBreakHandler/NullConfig.java b/src/main/java/ChinBreakHandler/NullConfig.java new file mode 100644 index 00000000..3e1286e9 --- /dev/null +++ b/src/main/java/ChinBreakHandler/NullConfig.java @@ -0,0 +1,8 @@ +package ChinBreakHandler; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; + +@ConfigGroup("chinBreakHandler") +public interface NullConfig extends Config { +} diff --git a/src/main/java/ChinBreakHandler/OptionsConfig.java b/src/main/java/ChinBreakHandler/OptionsConfig.java new file mode 100644 index 00000000..bfdb1332 --- /dev/null +++ b/src/main/java/ChinBreakHandler/OptionsConfig.java @@ -0,0 +1,150 @@ +package ChinBreakHandler; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; + +@ConfigGroup("chinBreakHandler") +public interface OptionsConfig extends Config { + @ConfigSection( + name = "Misc", + description = "", + position = 0 + ) + String misc = "Misc"; + + @ConfigItem( + keyName = "stopAfterBreaks", + name = "Stop after x breaks", + description = "Stop after a given amount of breaks (0 to disable)", + position = 1, + section = misc + ) + default int stopAfterBreaks() + { + return 0; + } + + @ConfigItem( + keyName = "autoBankPin", + name = "Auto Bank Pin", + description = "Will automatically enter your bank pin", + position = 2, + section = misc + ) + default boolean autoBankPin() { + return false; + } + + @ConfigItem( + keyName = "autoLoginOnDisconnect", + name = "Auto Login", + description = "Will automatically log you in if you disconnect while a break is planned", + position = 2, + section = misc + ) + default boolean autoLoginOnDisconnect() { + return false; + } + + @ConfigItem( + keyName = "avoidWorldsPlayerCount", + name = "Max Players", + description = "Maximum amount of players a world should have when hopping", + position = 3, + section = misc + ) + default int avoidWorldsPlayerCount() { + return 1500; + } + + @ConfigItem( + keyName = "avoidWorldsNumbers", + name = "Avoid Worlds", + description = "World numbers you want to avoid hopping to separated by commas.", + position = 4, + section = misc + ) + default String avoidWorldsNumbers() { + return "302,330"; + } + + @ConfigSection( + name = "Hopping", + description = "", + position = 2 + ) + String hoppingTitle = "Hopping"; + + @ConfigItem( + keyName = "hop-after-break", + name = "Hop world after break", + description = "Hop to a different world after taking a break", + position = 3, + section = hoppingTitle + ) + default boolean hopAfterBreak() + { + return false; + } + + @ConfigItem( + keyName = "useF2PWorlds", + name = "Use F2P Worlds Only", + description = "Restrict world hopping and login to free-to-play worlds only", + position = 1 + ) + default boolean useF2PWorlds() + { + return false; + } + + @ConfigItem( + keyName = "american", + name = "American", + description = "Enable hopping to American worlds", + position = 4, + section = hoppingTitle + ) + default boolean american() + { + return false; + } + + @ConfigItem( + keyName = "united-kingdom", + name = "United kingdom", + description = "Enable hopping to UK worlds", + position = 5, + section = hoppingTitle + ) + default boolean unitedKingdom() + { + return false; + } + + @ConfigItem( + keyName = "german", + name = "German", + description = "Enable hopping to German worlds", + position = 6, + section = hoppingTitle + ) + default boolean german() + { + return false; + } + + @ConfigItem( + keyName = "australian", + name = "Australian", + description = "Enable hopping to Australian worlds", + position = 7, + section = hoppingTitle + ) + default boolean australian() + { + return false; + } +} diff --git a/src/main/java/ChinBreakHandler/State.java b/src/main/java/ChinBreakHandler/State.java new file mode 100644 index 00000000..fd66e502 --- /dev/null +++ b/src/main/java/ChinBreakHandler/State.java @@ -0,0 +1,21 @@ +package ChinBreakHandler; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter(AccessLevel.PACKAGE) +@AllArgsConstructor +public enum State +{ + NULL, + + LOGIN_SCREEN, + INVENTORY, + RESUME, + + LOGOUT, + LOGOUT_TAB, + LOGOUT_BUTTON, + LOGOUT_WAIT +} diff --git a/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerAccountPanel.java b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerAccountPanel.java new file mode 100644 index 00000000..dc7184cc --- /dev/null +++ b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerAccountPanel.java @@ -0,0 +1,300 @@ +package ChinBreakHandler.ui; + +import com.google.inject.Inject; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.events.ConfigChanged; +import ChinBreakHandler.ChinBreakHandler; +import ChinBreakHandler.ChinBreakHandlerPlugin; +import ChinBreakHandler.util.DeferredDocumentChangedListener; +import ChinBreakHandler.util.ProfilesData; +import net.runelite.client.ui.PluginPanel; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.DocumentFilter; +import javax.swing.text.PlainDocument; +import java.awt.*; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; + +import static net.runelite.client.ui.PluginPanel.PANEL_WIDTH; + +public class ChinBreakHandlerAccountPanel extends JPanel +{ + @Override + public Dimension getPreferredSize() + { + return new Dimension(PANEL_WIDTH, super.getPreferredSize().height); + } + + private final ConfigManager configManager; + private final ChinBreakHandler chinBreakHandler; + private final JPanel contentPanel = new JPanel(new GridLayout(0, 1)); + + @Inject + ChinBreakHandlerAccountPanel(ChinBreakHandlerPlugin chinBreakHandlerPluginPlugin, ChinBreakHandler chinBreakHandler) + { + this.configManager = chinBreakHandlerPluginPlugin.getConfigManager(); + this.chinBreakHandler = chinBreakHandler; + + setupDefaults(); + + setLayout(new BorderLayout()); + setBackground(ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR); + + init(); + } + + private LoginMode getConfigValue() + { + String accountselection = configManager.getConfiguration("chinBreakHandler", "accountselection"); + + return LoginMode.parse(accountselection); + } + + private void init() { + contentPanel.setBorder(new EmptyBorder(10, 10, 0, 10)); + + JPanel accountSelection = new JPanel(new GridLayout(0, 2)); + accountSelection.setBorder(new EmptyBorder(5, 0, 0, 0)); + ButtonGroup buttonGroup = new ButtonGroup(); + + JCheckBox manualButton = new JCheckBox("Manual"); + JCheckBox profilesButton = new JCheckBox("Profiles plugin"); + JCheckBox jagexLauncherButton = new JCheckBox("Jagex Launcher"); + + String profilesSalt = configManager.getConfiguration("piggyProfiles", "salt"); + boolean profilesSavePasswords = Boolean.parseBoolean(configManager.getConfiguration("piggyProfiles", "rememberPassword")); + + String jagexLauncherStr = configManager.getConfiguration("chinBreakHandler", "jagexLauncher"); + if (jagexLauncherStr == null || jagexLauncherStr.isEmpty()) { + configManager.setConfiguration("chinBreakHandler", "jagexLauncher", false); + } + + String accountSelectionString = configManager.getConfiguration("chinBreakHandler", "accountselection"); + boolean jagexLauncher = configManager.getConfiguration("chinBreakHandler", "jagexLauncher", Boolean.class); + + if (jagexLauncher && getConfigValue() == null) { + configManager.setConfiguration("chinBreakHandler", "accountselection", LoginMode.LAUNCHER); + } else if (accountSelectionString.equalsIgnoreCase("true")) { + configManager.setConfiguration("chinBreakHandler", "accountselection", LoginMode.MANUAL); + } else if (accountSelectionString.equalsIgnoreCase("false")) { + configManager.setConfiguration("chinBreakHandler", "accountselection", LoginMode.PROFILES); + } + + if (profilesSalt == null || profilesSalt.length() == 0 || !profilesSavePasswords) + { + configManager.setConfiguration("chinBreakHandler", "accountselection", LoginMode.MANUAL); + profilesButton.setEnabled(false); + } + + manualButton.addActionListener(e -> { + configManager.setConfiguration("chinBreakHandler", "accountselection", LoginMode.MANUAL); + contentPanel(LoginMode.MANUAL); + }); + + profilesButton.addActionListener(e -> { + configManager.setConfiguration("chinBreakHandler", "accountselection", LoginMode.PROFILES); + contentPanel(LoginMode.PROFILES); + }); + + jagexLauncherButton.addActionListener(e -> { + configManager.setConfiguration("chinBreakHandler", "accountselection", LoginMode.PROFILES); + contentPanel(LoginMode.LAUNCHER); + }); + + buttonGroup.add(manualButton); + buttonGroup.add(profilesButton); + buttonGroup.add(jagexLauncherButton); + + LoginMode config = getConfigValue(); + + manualButton.setSelected(config == LoginMode.MANUAL); + profilesButton.setSelected(config == LoginMode.PROFILES); + jagexLauncherButton.setSelected(config == LoginMode.LAUNCHER); + + accountSelection.add(manualButton); + accountSelection.add(profilesButton); + accountSelection.add(jagexLauncherButton); + + add(accountSelection, BorderLayout.NORTH); + + contentPanel(config); + + add(contentPanel, BorderLayout.CENTER); + } + + private void emptyContentPanel() { + contentPanel.removeAll(); + contentPanel.revalidate(); + contentPanel.repaint(); + } + + private void contentPanel(LoginMode mode) + { + contentPanel.removeAll(); + + if (mode == LoginMode.MANUAL) + { + contentPanel.add(new JLabel("Username")); + + final JTextField usernameField = new JTextField(); + usernameField.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + usernameField.setText(configManager.getConfiguration("chinBreakHandler", "accountselection-manual-username")); + DeferredDocumentChangedListener usernameListener = new DeferredDocumentChangedListener(); + usernameListener.addChangeListener(e -> + configManager.setConfiguration("chinBreakHandler", "accountselection-manual-username", usernameField.getText())); + usernameField.getDocument().addDocumentListener(usernameListener); + + contentPanel.add(usernameField); + + contentPanel.add(new JLabel("Password")); + + final JPasswordField passwordField = new JPasswordField(); + passwordField.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + passwordField.setText(configManager.getConfiguration("chinBreakHandler", "accountselection-manual-password")); + DeferredDocumentChangedListener passwordListener = new DeferredDocumentChangedListener(); + passwordListener.addChangeListener(e -> + configManager.setConfiguration("chinBreakHandler", "accountselection-manual-password", String.valueOf(passwordField.getPassword()))); + passwordField.getDocument().addDocumentListener(passwordListener); + + contentPanel.add(passwordField); + + contentPanel.add(new JLabel("Bank Pin")); + + final JPasswordField pinField = new JPasswordField(); + pinField.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); + pinField.setText(configManager.getConfiguration("chinBreakHandler", "accountselection-manual-pin")); + PlainDocument document = (PlainDocument) pinField.getDocument(); + document.setDocumentFilter(new DocumentFilter() { + @Override + public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException { + String string = fb.getDocument().getText(0, fb.getDocument().getLength()) + text; + + if (string.length() <= 4) { + super.replace(fb, offset, length, text, attrs); + } + } + }); + + DeferredDocumentChangedListener pinListener = new DeferredDocumentChangedListener(); + pinListener.addChangeListener(e -> { + configManager.setConfiguration("chinBreakHandler", "accountselection-manual-pin", String.valueOf(pinField.getPassword())); + }); + pinField.getDocument().addDocumentListener(pinListener); + + contentPanel.add(pinField); + } + else if (ChinBreakHandlerPlugin.data == null && mode == LoginMode.PROFILES) + { + contentPanel.add(new JLabel("Profiles plugin password")); + final JPasswordField passwordField = new JPasswordField(); + passwordField.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + JLabel parsingLabel = new JLabel(); + parsingLabel.setHorizontalAlignment(SwingConstants.CENTER); + parsingLabel.setPreferredSize(new Dimension(PANEL_WIDTH, 15)); + + DeferredDocumentChangedListener passwordListener = new DeferredDocumentChangedListener(); + passwordListener.addChangeListener(e -> + { + try + { + ChinBreakHandlerPlugin.data = ProfilesData.getProfileData(configManager, passwordField.getPassword()); + contentPanel(LoginMode.PROFILES); + } + catch (InvalidKeySpecException | NoSuchPaddingException | BadPaddingException | InvalidKeyException | IllegalBlockSizeException | NoSuchAlgorithmException ignored) + { + parsingLabel.setText("Incorrect password!"); + } + }); + passwordField.getDocument().addDocumentListener(passwordListener); + + contentPanel.add(passwordField); + contentPanel.add(parsingLabel); + } else if (mode == LoginMode.LAUNCHER) { + emptyContentPanel(); + } + else + { + ConfigChanged configChanged = new ConfigChanged(); + configChanged.setGroup("mock"); + configChanged.setKey("mock"); + chinBreakHandler.configChanged.onNext(configChanged); + + if (!ChinBreakHandlerPlugin.data.contains(":")) + { + contentPanel.add(new JLabel("No accounts found")); + } + else + { + contentPanel.add(new JLabel("Select account")); + + String[] accounts = Arrays.stream(ChinBreakHandlerPlugin.data.split("\\n")) + .map((s) -> s.split(":")[0]) + .sorted() + .toArray(String[]::new); + + JComboBox filterComboBox = new JComboBox<>(accounts); + filterComboBox.setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH - 20, 30)); + filterComboBox.addActionListener(e -> { + if (filterComboBox.getSelectedItem() != null) + { + configManager.setConfiguration("chinBreakHandler", "accountselection-profiles-account", filterComboBox.getSelectedItem().toString()); + } + }); + + String config = configManager.getConfiguration("chinBreakHandler", "accountselection-profiles-account"); + + if (config != null) + { + int index = Arrays.asList(accounts).indexOf(config); + + if (index != -1) + { + filterComboBox.setSelectedIndex(index); + } + else + { + filterComboBox.setSelectedIndex(0); + } + } + + contentPanel.add(filterComboBox); + } + } + + contentPanel.revalidate(); + contentPanel.repaint(); + } + + private void setupDefaults() + { + if (configManager.getConfiguration("chinBreakHandler", "accountselection") == null) + { + configManager.setConfiguration("chinBreakHandler", "accountselection", true); + } + + if (configManager.getConfiguration("chinBreakHandler", "accountselection-manual-username") == null) + { + configManager.setConfiguration("chinBreakHandler", "accountselection-manual-username", ""); + } + + if (configManager.getConfiguration("chinBreakHandler", "accountselection-manual-password") == null) + { + configManager.setConfiguration("chinBreakHandler", "accountselection-manual-password", ""); + } + + if (configManager.getConfiguration("chinBreakHandler", "accountselection-profiles-account") == null) + { + configManager.setConfiguration("chinBreakHandler", "accountselection-profiles-account", ""); + } + } +} \ No newline at end of file diff --git a/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPanel.java b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPanel.java new file mode 100644 index 00000000..48fc3909 --- /dev/null +++ b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPanel.java @@ -0,0 +1,388 @@ +package ChinBreakHandler.ui; + +import com.google.inject.Inject; +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.disposables.Disposable; +import ChinBreakHandler.ChinBreakHandler; +import ChinBreakHandler.ChinBreakHandlerPlugin; +import ChinBreakHandler.util.ConfigPanel; +import ChinBreakHandler.util.JMultilineLabel; +import ChinBreakHandler.util.SwingUtilExtended; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.PluginErrorPanel; +import net.runelite.client.util.ImageUtil; + +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.time.Instant; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class ChinBreakHandlerPanel extends PluginPanel { + public final static Color PANEL_BACKGROUND_COLOR = ColorScheme.DARK_GRAY_COLOR; + final static Color BACKGROUND_COLOR = ColorScheme.DARKER_GRAY_COLOR; + + static final Font NORMAL_FONT = FontManager.getRunescapeFont(); + static final Font SMALL_FONT = FontManager.getRunescapeSmallFont(); + + private static final ImageIcon HELP_ICON; + private static final ImageIcon HELP_HOVER_ICON; + + static + { + + final BufferedImage helpIcon = + ImageUtil.recolorImage( + ImageUtil.loadImageResource(ChinBreakHandlerPlugin.class, "/help.png"), + ColorScheme.BRAND_ORANGE + ); + HELP_ICON = new ImageIcon(helpIcon); + HELP_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(helpIcon, 0.53f)); + } + + private final ChinBreakHandlerPlugin chinBreakHandlerPluginPlugin; + private final ChinBreakHandler chinBreakHandler; + private final ConfigPanel configPanel; + + public @NonNull Disposable pluginDisposable; + public @NonNull Disposable activeDisposable; + public @NonNull Disposable currentDisposable; + public @NonNull Disposable startDisposable; + public @NonNull Disposable configDisposable; + + private final JPanel unlockAccountPanel = new JPanel(new BorderLayout()); + private final JPanel breakTimingsPanel = new JPanel(new GridLayout(0, 1)); + + @Inject + private ChinBreakHandlerPanel(ChinBreakHandlerPlugin chinBreakHandlerPluginPlugin, ChinBreakHandler chinBreakHandler, ConfigPanel configPanel) + { + super(false); + + configPanel.init(chinBreakHandlerPluginPlugin.getOptionsConfig()); + + this.chinBreakHandlerPluginPlugin = chinBreakHandlerPluginPlugin; + this.chinBreakHandler = chinBreakHandler; + this.configPanel = configPanel; + + pluginDisposable = chinBreakHandler + .getPluginObservable() + .subscribe((Map plugins) -> + SwingUtilExtended.syncExec(() -> + buildPanel(plugins))); + + activeDisposable = chinBreakHandler + .getActiveObservable() + .subscribe( + (ignored) -> + SwingUtilExtended.syncExec(() -> + buildPanel(chinBreakHandler.getPlugins())) + ); + + currentDisposable = chinBreakHandler + .getActiveBreaksObservable() + .subscribe( + (ignored) -> + SwingUtilExtended.syncExec(() -> + buildPanel(chinBreakHandler.getPlugins())) + ); + + startDisposable = chinBreakHandler + .getActiveObservable() + .subscribe( + (ignored) -> + SwingUtilExtended.syncExec(() -> { + unlockAccountsPanel(); + unlockAccountPanel.revalidate(); + unlockAccountPanel.repaint(); + + breakTimingsPanel(); + breakTimingsPanel.revalidate(); + breakTimingsPanel.repaint(); + }) + ); + + configDisposable = chinBreakHandler + .configChanged + .subscribe( + (ignored) -> + SwingUtilExtended.syncExec(() -> { + unlockAccountsPanel(); + unlockAccountPanel.revalidate(); + unlockAccountPanel.repaint(); + }) + ); + + this.setBackground(PANEL_BACKGROUND_COLOR); + this.setLayout(new BorderLayout()); + + buildPanel(chinBreakHandler.getPlugins()); + } + + void buildPanel(Map plugins) + { + removeAll(); + + if (plugins.isEmpty()) + { + PluginErrorPanel errorPanel = new PluginErrorPanel(); + errorPanel.setContent("Chin break handler", "There were no plugins that registered themselves with the break handler."); + + add(errorPanel, BorderLayout.NORTH); + } + else + { + JPanel contentPanel = new JPanel(new BorderLayout()); + + contentPanel.add(statusPanel(), BorderLayout.NORTH); + contentPanel.add(tabbedPane(plugins), BorderLayout.CENTER); + + add(titleBar(), BorderLayout.NORTH); + add(contentPanel, BorderLayout.CENTER); + } + + revalidate(); + repaint(); + } + + private JPanel titleBar() + { + JPanel titlePanel = new JPanel(new BorderLayout()); + titlePanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + JLabel title = new JLabel(); + JLabel help = new JLabel(HELP_ICON); + + title.setText("Chin Break Handler"); + title.setForeground(Color.WHITE); + + help.setToolTipText("Info"); + help.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + JOptionPane.showMessageDialog( + chinBreakHandlerPluginPlugin.getFrame(), + "
The configs in this panel can be used to schedule breaks.
" + + "When the timer hits zero a break is scheduled. This does not mean that the break will be taken immediately!
" + + "Plugins decide what the best time is for a break, for example a NMZ dream will be finished instead of interrupted.
", + "Chin break handler", + JOptionPane.QUESTION_MESSAGE + ); + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + help.setIcon(HELP_HOVER_ICON); + } + + @Override + public void mouseExited(MouseEvent mouseEvent) + { + help.setIcon(HELP_ICON); + } + }); + help.setBorder(new EmptyBorder(0, 3, 0, 0)); + + titlePanel.add(title, BorderLayout.WEST); + titlePanel.add(help, BorderLayout.EAST); + + return titlePanel; + } + + private boolean unlockAccountsPanel() + { + Set activePlugins = chinBreakHandler.getActivePlugins(); + + LoginMode loginMode = LoginMode.parse(chinBreakHandlerPluginPlugin.getConfigManager().getConfiguration("chinBreakHandler", "accountselection")); + + String data = ChinBreakHandlerPlugin.data; + + if (activePlugins.isEmpty() || loginMode != LoginMode.PROFILES || (data != null && !data.trim().isEmpty())) + { + return false; + } + + return true; + } + + private boolean breakTimingsPanel() + { + breakTimingsPanel.removeAll(); + + Set pluginStream = chinBreakHandler.getActivePlugins().stream().filter(e -> !chinBreakHandlerPluginPlugin.isValidBreak(e)).collect(Collectors.toSet()); + + if (pluginStream.isEmpty()) + { + return false; + } + + for (Plugin plugin : pluginStream) + { + JPanel wrapperPanel = new JPanel(new BorderLayout()); + + JPanel titleWrapper = new JPanel(new BorderLayout()); + titleWrapper.setBackground(new Color(125, 40, 40)); + titleWrapper.setBorder(new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(115, 30, 30)), + BorderFactory.createLineBorder(new Color(125, 40, 40)) + )); + + JLabel title = new JLabel(); + title.setText("Warning"); + title.setFont(NORMAL_FONT); + title.setPreferredSize(new Dimension(0, 24)); + title.setForeground(Color.WHITE); + title.setBorder(new EmptyBorder(0, 8, 0, 0)); + + titleWrapper.add(title, BorderLayout.CENTER); + + wrapperPanel.add(titleWrapper, BorderLayout.NORTH); + + JPanel contentPanel = new JPanel(new BorderLayout()); + contentPanel.setBackground(new Color(125, 40, 40)); + + JMultilineLabel description = new JMultilineLabel(); + + description.setText("The break timings for " + plugin.getName() + " are invalid!"); + description.setFont(SMALL_FONT); + description.setDisabledTextColor(Color.WHITE); + description.setBackground(new Color(115, 30, 30)); + + description.setBorder(new EmptyBorder(5, 5, 10, 5)); + + contentPanel.add(description, BorderLayout.CENTER); + + wrapperPanel.add(contentPanel, BorderLayout.CENTER); + + breakTimingsPanel.add(wrapperPanel); + } + + return true; + } + + private JPanel statusPanel() + { + Set activePlugins = chinBreakHandler.getActivePlugins(); + + JPanel contentPanel = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + if (unlockAccountsPanel()) + { + JOptionPane.showMessageDialog(null, "Please enter your profiles password to allow break handling."); + } + + if (breakTimingsPanel()) + { + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridy += 1; + c.insets = new Insets(5, 10, 0, 10); + + contentPanel.add(breakTimingsPanel, c); + } + + if (activePlugins.isEmpty()) + { + return contentPanel; + } + + for (Plugin plugin : activePlugins) + { + ChinBreakHandlerStatusPanel statusPanel = new ChinBreakHandlerStatusPanel(chinBreakHandlerPluginPlugin, chinBreakHandler, plugin); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridy += 1; + c.insets = new Insets(5, 10, 0, 10); + + contentPanel.add(statusPanel, c); + } + + JButton scheduleBreakButton = new JButton("Schedule break now"); + + if (activePlugins.size() > 0) + { + scheduleBreakButton.addActionListener(e -> activePlugins.forEach(plugin -> { + if (!chinBreakHandler.isBreakActive(plugin)) + { + chinBreakHandler.planBreak(plugin, Instant.now()); + } + })); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridy += 1; + c.insets = new Insets(5, 10, 0, 10); + + contentPanel.add(scheduleBreakButton, c); + } + + return contentPanel; + } + + private JTabbedPane tabbedPane(Map plugins) + { + JTabbedPane mainTabPane = new JTabbedPane(); + + JScrollPane pluginPanel = wrapContainer(contentPane(plugins)); + JScrollPane repositoryPanel = wrapContainer(new ChinBreakHandlerAccountPanel(chinBreakHandlerPluginPlugin, chinBreakHandler)); + JScrollPane optionsPanel = wrapContainer(configPanel); + + mainTabPane.add("Plugins", pluginPanel); + mainTabPane.add("Accounts", repositoryPanel); + mainTabPane.add("Options", optionsPanel); + + return mainTabPane; + } + + private JPanel contentPane(Map plugins) + { + JPanel contentPanel = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + if (chinBreakHandler.getPlugins().isEmpty()) + { + return contentPanel; + } + + for (Map.Entry plugin : plugins.entrySet()) + { + ChinBreakHandlerPluginPanel panel = new ChinBreakHandlerPluginPanel(chinBreakHandlerPluginPlugin, plugin.getKey(), plugin.getValue()); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.gridy += 1; + c.insets = new Insets(5, 10, 0, 10); + + contentPanel.add(panel, c); + } + + return contentPanel; + } + + public static JScrollPane wrapContainer(final JPanel container) + { + final JPanel wrapped = new JPanel(new BorderLayout()); + wrapped.add(container, BorderLayout.NORTH); + wrapped.setBackground(PANEL_BACKGROUND_COLOR); + + final JScrollPane scroller = new JScrollPane(wrapped); + scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scroller.getVerticalScrollBar().setPreferredSize(new Dimension(8, 0)); + scroller.setBackground(PANEL_BACKGROUND_COLOR); + + return scroller; + } +} diff --git a/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPluginPanel.java b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPluginPanel.java new file mode 100644 index 00000000..4025718a --- /dev/null +++ b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerPluginPanel.java @@ -0,0 +1,315 @@ +package ChinBreakHandler.ui; + +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.Units; +import ChinBreakHandler.ChinBreakHandlerPlugin; +import ChinBreakHandler.util.JMultilineLabel; +import ChinBreakHandler.util.OnOffToggleButton; +import ChinBreakHandler.util.UnitFormatterFactory; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.PluginPanel; + +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import java.awt.*; + +public class ChinBreakHandlerPluginPanel extends JPanel +{ + private final ConfigManager configManager; + private final Plugin plugin; + private final boolean configurable; + + @Override + public Dimension getPreferredSize() + { + return new Dimension(PluginPanel.PANEL_WIDTH, super.getPreferredSize().height); + } + + ChinBreakHandlerPluginPanel(ChinBreakHandlerPlugin chinBreakHandlerPluginPlugin, Plugin plugin, boolean configurable) + { + this.configManager = chinBreakHandlerPluginPlugin.getConfigManager(); + this.plugin = plugin; + this.configurable = configurable; + + if (configurable) + { + setupDefaults(); + } + + setLayout(new BorderLayout()); + setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + + init(); + } + + private void init() + { + JPanel titleWrapper = new JPanel(new BorderLayout()); + titleWrapper.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + titleWrapper.setBorder(new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR), + BorderFactory.createLineBorder(ChinBreakHandlerPanel.BACKGROUND_COLOR) + )); + + JLabel title = new JLabel(); + title.setText(plugin.getName()); + title.setFont(ChinBreakHandlerPanel.NORMAL_FONT); + title.setPreferredSize(new Dimension(0, 24)); + title.setForeground(Color.WHITE); + title.setBorder(new EmptyBorder(0, 8, 0, 0)); + + JPanel titleActions = new JPanel(new BorderLayout(3, 0)); + titleActions.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + titleActions.setBorder(new EmptyBorder(0, 0, 0, 8)); + + if (configurable) + { + String pluginName = ChinBreakHandlerPlugin.sanitizedName(plugin); + + JToggleButton onOffToggle = new OnOffToggleButton(); + + onOffToggle.setSelected(Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", pluginName + "-enabled"))); + onOffToggle.addItemListener(i -> + configManager.setConfiguration("chinBreakHandler", pluginName + "-enabled", onOffToggle.isSelected())); + + titleActions.add(onOffToggle, BorderLayout.EAST); + } + + titleWrapper.add(title, BorderLayout.CENTER); + titleWrapper.add(titleActions, BorderLayout.EAST); + + add(titleWrapper, BorderLayout.NORTH); + + if (configurable) + { + add(breakPanel(), BorderLayout.CENTER); + add(typePanel(), BorderLayout.SOUTH); + } + else + { + add(notConfigurable(), BorderLayout.CENTER); + } + } + + private JSpinner createSpinner(int value) + { + SpinnerModel model = new SpinnerNumberModel(value, 0, Integer.MAX_VALUE, 1); + JSpinner spinner = new JSpinner(model); + Component editor = spinner.getEditor(); + JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField(); + spinnerTextField.setColumns(4); + spinnerTextField.setFormatterFactory(new UnitFormatterFactory(Units.MINUTES)); + + return spinner; + } + + private JPanel notConfigurable() + { + JPanel contentPanel = new JPanel(new BorderLayout()); + contentPanel.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + JMultilineLabel description = new JMultilineLabel(); + + description.setText("The break timings for this plugin are not configurable."); + description.setFont(ChinBreakHandlerPanel.SMALL_FONT); + description.setDisabledTextColor(Color.WHITE); + description.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + + contentPanel.add(description, BorderLayout.CENTER); + + return contentPanel; + } + + private JPanel breakPanel() + { + String pluginName = ChinBreakHandlerPlugin.sanitizedName(plugin); + + JPanel contentPanel = new JPanel(new GridBagLayout()); + contentPanel.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + contentPanel.setBorder(new CompoundBorder( + new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR), + BorderFactory.createLineBorder(ChinBreakHandlerPanel.BACKGROUND_COLOR) + ), BorderFactory.createEmptyBorder(5, 5, 5, 5))); + + JSpinner thresholdFrom = createSpinner( + parseInt(configManager.getConfiguration("chinBreakHandler", pluginName + "-thresholdfrom"), 60) + ); + + JSpinner thresholdTo = createSpinner( + parseInt(configManager.getConfiguration("chinBreakHandler", pluginName + "-thresholdto"), 120) + ); + + JSpinner breakFrom = createSpinner( + parseInt(configManager.getConfiguration("chinBreakHandler", pluginName + "-breakfrom"), 10) + ); + + JSpinner breakTo = createSpinner( + parseInt(configManager.getConfiguration("chinBreakHandler", pluginName + "-breakto"), 15) + ); + + thresholdFrom.addChangeListener(e -> + configManager.setConfiguration("chinBreakHandler", pluginName + "-thresholdfrom", thresholdFrom.getValue())); + + thresholdTo.addChangeListener(e -> + configManager.setConfiguration("chinBreakHandler", pluginName + "-thresholdto", thresholdTo.getValue())); + + breakFrom.addChangeListener(e -> + configManager.setConfiguration("chinBreakHandler", pluginName + "-breakfrom", breakFrom.getValue())); + + breakTo.addChangeListener(e -> + configManager.setConfiguration("chinBreakHandler", pluginName + "-breakto", breakTo.getValue())); + + GridBagConstraints c = new GridBagConstraints(); + + + c.insets = new Insets(2, 0, 2, 0); + c.fill = GridBagConstraints.CENTER; + c.weightx = 0; + c.gridwidth = 3; + c.gridx = 0; + c.gridy = 0; + contentPanel.add(new JLabel("After running for"), c); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1; + c.gridwidth = 1; + c.gridx = 0; + c.gridy = 1; + contentPanel.add(thresholdFrom, c); + + c.fill = GridBagConstraints.CENTER; + c.weightx = 0.75; + c.gridwidth = 1; + c.gridx = 1; + c.gridy = 1; + contentPanel.add(new JLabel(" - "), c); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1; + c.gridwidth = 1; + c.gridx = 2; + c.gridy = 1; + contentPanel.add(thresholdTo, c); + + c.insets = new Insets(8, 0, 2, 0); + c.fill = GridBagConstraints.CENTER; + c.weightx = 0; + c.gridwidth = 3; + c.gridx = 0; + c.gridy = 2; + contentPanel.add(new JLabel("Schedule a break for"), c); + + c.insets = new Insets(2, 0, 2, 0); + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1; + c.gridwidth = 1; + c.gridx = 0; + c.gridy = 3; + contentPanel.add(breakFrom, c); + + c.fill = GridBagConstraints.CENTER; + c.weightx = 0.75; + c.gridwidth = 1; + c.gridx = 1; + c.gridy = 3; + contentPanel.add(new JLabel(" - "), c); + + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1; + c.gridwidth = 1; + c.gridx = 2; + c.gridy = 3; + contentPanel.add(breakTo, c); + + return contentPanel; + } + + private JPanel typePanel() + { + String pluginName = ChinBreakHandlerPlugin.sanitizedName(plugin); + + JPanel contentPanel = new JPanel(new GridLayout(0, 2)); + contentPanel.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + ButtonGroup buttonGroup = new ButtonGroup(); + + JCheckBox logoutButton = new JCheckBox("Logout"); + JCheckBox afkButton = new JCheckBox("AFK"); + + boolean logout = Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", pluginName + "-logout")); + + logoutButton.setSelected(logout); + afkButton.setSelected(!logout); + + logoutButton.addActionListener(e -> + configManager.setConfiguration("chinBreakHandler", pluginName + "-logout", logoutButton.isSelected())); + + afkButton.addActionListener(e -> + configManager.setConfiguration("chinBreakHandler", pluginName + "-logout", !afkButton.isSelected())); + + buttonGroup.add(logoutButton); + buttonGroup.add(afkButton); + + contentPanel.add(logoutButton); + contentPanel.add(afkButton); + + return contentPanel; + } + + private void setupDefaults() + { + String pluginName = ChinBreakHandlerPlugin.sanitizedName(plugin); + + String enabled = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-thresholdfrom"); + String logout = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-logout"); + + String thresholdfrom = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-thresholdfrom"); + String thresholdto = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-thresholdto"); + String breakfrom = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-breakfrom"); + String breakto = configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-breakto"); + + if (enabled == null) + { + configManager.setConfiguration("chinBreakHandler", pluginName + "-enabled", false); + } + + if (logout == null) + { + configManager.setConfiguration("chinBreakHandler", pluginName + "-logout", true); + } + + if (!ChinBreakHandlerPlugin.isNumeric(thresholdfrom) || (ChinBreakHandlerPlugin.isNumeric(thresholdfrom) && Integer.parseInt(thresholdfrom) < 0)) + { + configManager.setConfiguration("chinBreakHandler", pluginName + "-thresholdfrom", 60); + } + + if (!ChinBreakHandlerPlugin.isNumeric(thresholdto) || (ChinBreakHandlerPlugin.isNumeric(thresholdto) && Integer.parseInt(thresholdto) < 0)) + { + configManager.setConfiguration("chinBreakHandler", pluginName + "-thresholdto", 120); + } + + if (!ChinBreakHandlerPlugin.isNumeric(breakfrom) || (ChinBreakHandlerPlugin.isNumeric(breakfrom) && Integer.parseInt(breakfrom) < 0)) + { + configManager.setConfiguration("chinBreakHandler", pluginName + "-breakfrom", 10); + } + + if (!ChinBreakHandlerPlugin.isNumeric(breakto) || (ChinBreakHandlerPlugin.isNumeric(breakto) && Integer.parseInt(breakto) < 0)) + { + configManager.setConfiguration("chinBreakHandler", pluginName + "-breakto", 15); + } + } + + private int parseInt(String value, int def) + { + try + { + return Integer.parseInt(value); + } + catch (NumberFormatException e) + { + return def; + } + } +} diff --git a/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerStatusPanel.java b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerStatusPanel.java new file mode 100644 index 00000000..b2819470 --- /dev/null +++ b/src/main/java/ChinBreakHandler/ui/ChinBreakHandlerStatusPanel.java @@ -0,0 +1,419 @@ +package ChinBreakHandler.ui; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.ConfigManager; +import ChinBreakHandler.ChinBreakHandler; +import ChinBreakHandler.ChinBreakHandlerPlugin; +import ChinBreakHandler.util.SwingUtilExtended; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.events.ConfigChanged; + + +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class ChinBreakHandlerStatusPanel extends JPanel +{ + private final ConfigManager configManager; + private final ChinBreakHandlerPlugin chinBreakHandlerPluginPlugin; + private final ChinBreakHandler chinBreakHandler; + private final Plugin plugin; + + private final JPanel contentPanel = new JPanel(new GridBagLayout()); + private final JPanel infoWrapper = new JPanel(new BorderLayout()); + private final JPanel infoPanel = new JPanel(new GridBagLayout()); + private final JPanel extraDataPanel = new JPanel(new GridBagLayout()); + private final JLabel timeLabel = new JLabel(); + private final JLabel runtimeLabel = new JLabel(); + private final JLabel breaksLabel = new JLabel(); + + @Override + public Dimension getPreferredSize() + { + return new Dimension(PluginPanel.PANEL_WIDTH, super.getPreferredSize().height); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + ChinBreakHandlerStatusPanel(ChinBreakHandlerPlugin ChinBreakHandlerPlugin, ChinBreakHandler ChinBreakHandler, Plugin plugin) + { + this.configManager = ChinBreakHandlerPlugin.getConfigManager(); + this.chinBreakHandlerPluginPlugin = ChinBreakHandlerPlugin; + this.chinBreakHandler = ChinBreakHandler; + this.plugin = plugin; + + setLayout(new BorderLayout()); + setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + + ChinBreakHandler + .configChanged + .subscribe(this::onConfigChanged); + + if (ChinBreakHandlerPlugin.disposables.containsKey(plugin)) + { + Disposable seconds = ChinBreakHandlerPlugin.disposables.get(plugin); + + if (!seconds.isDisposed()) + { + seconds.dispose(); + } + } + + Disposable secondsDisposable = Observable + .interval(500, TimeUnit.MILLISECONDS) + .subscribe(this::milliseconds); + + ChinBreakHandlerPlugin.disposables.put(plugin, secondsDisposable); + + Disposable extraDataDisposable = ChinBreakHandler + .getExtraDataObservable() + .subscribe((data) -> SwingUtilExtended.syncExec(() -> this.extraData(data))); + + ChinBreakHandlerPlugin.disposables.put(plugin, extraDataDisposable); + + init(); + } + + public static String formatDuration(Duration duration) + { + long seconds = duration.getSeconds(); + long absSeconds = Math.abs(seconds); + + return String.format( + "%02d:%02d:%02d", + absSeconds / 3600, + (absSeconds % 3600) / 60, + absSeconds % 60); + } + + private void milliseconds(long ignored) + { + Instant now = Instant.now(); + + Map startTimes = chinBreakHandler.getStartTimes(); + + if (startTimes.containsKey(plugin)) + { + Duration duration = Duration.between(chinBreakHandler.getStartTimes().get(plugin), now); + runtimeLabel.setText(formatDuration(duration)); + } + + Map breaks = chinBreakHandler.getAmountOfBreaks(); + + if (breaks.containsKey(plugin)) + { + breaksLabel.setText(String.valueOf(chinBreakHandler.getAmountOfBreaks().get(plugin))); + } + + if (!chinBreakHandler.isBreakPlanned(plugin) && !chinBreakHandler.isBreakActive(plugin)) + { + timeLabel.setText("00:00:00"); + return; + } + + if (chinBreakHandler.isBreakPlanned(plugin)) + { + Instant breaktime = chinBreakHandler.getPlannedBreak(plugin); + + if (now.isAfter(breaktime)) + { + timeLabel.setText("Waiting for plugin"); + } + else + { + Duration duration = Duration.between(now, breaktime); + timeLabel.setText(formatDuration(duration)); + } + } + else if (chinBreakHandler.isBreakActive(plugin)) + { + Instant breaktime = chinBreakHandler.getActiveBreak(plugin); + + if (now.isAfter(breaktime)) + { + timeLabel.setText("00:00:00"); + } + else + { + Duration duration = Duration.between(now, breaktime); + timeLabel.setText(formatDuration(duration)); + } + } + else + { + timeLabel.setText("-"); + } + + boolean enabled = Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-enabled")); + + if (enabled && chinBreakHandler.getPlugins().get(plugin) && chinBreakHandlerPluginPlugin.isValidBreak(plugin) && !chinBreakHandler.isBreakPlanned(plugin) && !chinBreakHandler.isBreakActive(plugin)) + { + chinBreakHandlerPluginPlugin.scheduleBreak(plugin); + } + } + + private void extraData(Map> extraData) + { + Map pluginData = extraData.get(plugin); + + if (pluginData.isEmpty()) + { + return; + } + + extraDataPanel.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + extraDataPanel.setBorder(new CompoundBorder( + new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR), + BorderFactory.createLineBorder(ChinBreakHandlerPanel.BACKGROUND_COLOR) + ), BorderFactory.createEmptyBorder(5, 5, 5, 5))); + + extraDataPanel.removeAll(); + + GridBagConstraints c = new GridBagConstraints(); + + c.insets = new Insets(2, 0, 2, 0); + c.gridx = 0; + c.gridy = 0; + + pluginData.computeIfPresent("State", (k, v) -> { + JLabel keyLabel = new JLabel(k); + c.gridwidth = 2; + c.weightx = 2; + c.gridx = 0; + c.gridy = 0; + extraDataPanel.add(keyLabel, c); + + JLabel valueLabel = new JLabel(v); + c.weightx = 1; + c.gridx = 0; + c.gridy = 1; + extraDataPanel.add(valueLabel, c); + + c.gridx += 1; + + return null; + }); + + int index = 0; + for (Map.Entry data : pluginData.entrySet()) + { + index++; + + JLabel keyLabel = new JLabel(data.getKey()); + JLabel valueLabel = new JLabel(data.getValue()); + + boolean bump = index % 2 == 0; + + if (!bump && index == pluginData.size()) + { + c.gridwidth = 2; + } + else + { + c.gridwidth = 1; + } + + c.gridx = bump ? 1 : 0; + + c.weightx = 2; + if (bump) + { + c.gridy -= 1; + } + else + { + c.gridy += 1; + } + extraDataPanel.add(keyLabel, c); + + c.weightx = 1; + c.gridy += 1; + extraDataPanel.add(valueLabel, c); + } + + extraDataPanel.revalidate(); + extraDataPanel.repaint(); + } + + private void onConfigChanged(ConfigChanged configChanged) + { + if (configChanged == null || !configChanged.getGroup().equals("chinBreakHandler") || !configChanged.getKey().contains(ChinBreakHandlerPlugin.sanitizedName(plugin))) + { + return; + } + + if (configChanged.getKey().contains("enabled") && configChanged.getNewValue().equals("false")) + { + chinBreakHandler.removePlannedBreak(plugin); + } + + statusPanel(); + + contentPanel.revalidate(); + contentPanel.repaint(); + } + + private void init() + { + timeLabel.setFont(new Font("", Font.PLAIN, 20)); + + JPanel titleWrapper = new JPanel(new BorderLayout()); + titleWrapper.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + titleWrapper.setBorder(new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR), + BorderFactory.createLineBorder(ChinBreakHandlerPanel.BACKGROUND_COLOR) + )); + + JLabel title = new JLabel(); + title.setText(plugin.getName()); + title.setFont(ChinBreakHandlerPanel.NORMAL_FONT); + title.setPreferredSize(new Dimension(0, 24)); + title.setForeground(Color.WHITE); + title.setBorder(new EmptyBorder(0, 8, 0, 0)); + + JPanel titleActions = new JPanel(new BorderLayout(3, 0)); + titleActions.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + titleActions.setBorder(new EmptyBorder(0, 0, 0, 8)); + + JLabel status = new JLabel(); + status.setText("running"); + status.setFont(ChinBreakHandlerPanel.SMALL_FONT.deriveFont(16f)); + status.setForeground(Color.GREEN); + + titleActions.add(status, BorderLayout.EAST); + + titleWrapper.add(title, BorderLayout.CENTER); + titleWrapper.add(titleActions, BorderLayout.EAST); + + statusPanel(); + infoPanel(); + + infoWrapper.add(infoPanel, BorderLayout.NORTH); + infoWrapper.add(extraDataPanel, BorderLayout.SOUTH); + + add(titleWrapper, BorderLayout.NORTH); + add(contentPanel, BorderLayout.CENTER); + add(infoWrapper, BorderLayout.SOUTH); + } + + private void statusPanel() + { + contentPanel.removeAll(); + + contentPanel.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + contentPanel.setBorder(new CompoundBorder( + new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR), + BorderFactory.createLineBorder(ChinBreakHandlerPanel.BACKGROUND_COLOR) + ), BorderFactory.createEmptyBorder(5, 5, 5, 5))); + + GridBagConstraints c = new GridBagConstraints(); + + if (!chinBreakHandler.getPlugins().get(plugin) && !chinBreakHandler.isBreakActive(plugin)) + { + c.insets = new Insets(2, 0, 2, 0); + c.weightx = 0; + c.gridx = 0; + c.gridy = 0; + contentPanel.add(new JLabel("The plugin will handle break timings"), c); + + return; + } + + if (chinBreakHandler.getPlugins().get(plugin)) + { + boolean enabled = Boolean.parseBoolean(configManager.getConfiguration("chinBreakHandler", ChinBreakHandlerPlugin.sanitizedName(plugin) + "-enabled")); + + if (!enabled) + { + c.insets = new Insets(2, 0, 2, 0); + c.weightx = 0; + c.gridx = 0; + c.gridy = 0; + contentPanel.add(new JLabel("Breaks are disabled for this plugin"), c); + + return; + } + + if (chinBreakHandler.getPlugins().get(plugin) && chinBreakHandlerPluginPlugin.isValidBreak(plugin) && !chinBreakHandler.isBreakPlanned(plugin) && !chinBreakHandler.isBreakActive(plugin)) + { + chinBreakHandlerPluginPlugin.scheduleBreak(plugin); + } + } + + JLabel breaking = new JLabel(); + + if (chinBreakHandler.isBreakPlanned(plugin)) + { + breaking.setText("Scheduled break in:"); + } + else if (chinBreakHandler.isBreakActive(plugin)) + { + breaking.setText("Taking a break for:"); + } + + c.insets = new Insets(2, 0, 2, 0); + c.weightx = 0; + c.gridx = 0; + c.gridy = 0; + contentPanel.add(breaking, c); + + c.fill = GridBagConstraints.CENTER; + c.weightx = 0; + c.gridx = 0; + c.gridy = 1; + contentPanel.add(timeLabel, c); + } + + private void infoPanel() + { + infoPanel.removeAll(); + + infoPanel.setBackground(ChinBreakHandlerPanel.BACKGROUND_COLOR); + infoPanel.setBorder(new CompoundBorder( + new CompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR), + BorderFactory.createLineBorder(ChinBreakHandlerPanel.BACKGROUND_COLOR) + ), BorderFactory.createEmptyBorder(5, 5, 5, 5))); + + GridBagConstraints c = new GridBagConstraints(); + + c.insets = new Insets(2, 0, 2, 0); + c.weightx = 2; + c.gridx = 0; + c.gridy = 0; + + infoPanel.add(new JLabel("Total runtime"), c); + + c.insets = new Insets(2, 0, 2, 0); + c.weightx = 1; + c.gridx = 0; + c.gridy = 1; + + infoPanel.add(runtimeLabel, c); + + c.insets = new Insets(2, 0, 2, 0); + c.weightx = 2; + c.gridx = 1; + c.gridy = 0; + + infoPanel.add(new JLabel("Amount of breaks"), c); + + c.insets = new Insets(2, 0, 2, 0); + c.weightx = 1; + c.gridx = 1; + c.gridy = 1; + + infoPanel.add(breaksLabel, c); + } +} diff --git a/src/main/java/ChinBreakHandler/ui/LoginMode.java b/src/main/java/ChinBreakHandler/ui/LoginMode.java new file mode 100644 index 00000000..bfce6d32 --- /dev/null +++ b/src/main/java/ChinBreakHandler/ui/LoginMode.java @@ -0,0 +1,19 @@ +package ChinBreakHandler.ui; + +public enum LoginMode { + MANUAL, + PROFILES, + LAUNCHER; + + public static LoginMode parse(String s) { + if (s == null) { + return MANUAL; // fallback + } + for (LoginMode mode : LoginMode.values()) { + if (s.equalsIgnoreCase(mode.name())) { + return mode; + } + } + return MANUAL; + } +} \ No newline at end of file diff --git a/src/main/java/ChinBreakHandler/util/ConfigPanel.java b/src/main/java/ChinBreakHandler/util/ConfigPanel.java new file mode 100644 index 00000000..2ef9bcbe --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/ConfigPanel.java @@ -0,0 +1,449 @@ +package ChinBreakHandler.util; + +import com.google.common.collect.ComparisonChain; +import com.google.common.primitives.Ints; +import com.google.inject.Inject; +import io.reactivex.rxjava3.disposables.Disposable; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.Alpha; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigDescriptor; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigItemDescriptor; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.ConfigObject; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.ConfigSectionDescriptor; +import net.runelite.client.config.Range; +import net.runelite.client.config.Units; +import ChinBreakHandler.ChinBreakHandlerPlugin; +import ChinBreakHandler.ui.ChinBreakHandlerPanel; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.components.TitleCaseListCellRenderer; +import net.runelite.client.util.Text; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Stack; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import static net.runelite.client.ui.PluginPanel.PANEL_WIDTH; + + +@Slf4j +public class ConfigPanel extends FixedWidthPanel +{ + @Override + public Dimension getPreferredSize() + { + return new Dimension(PANEL_WIDTH, super.getPreferredSize().height); + } + + private static final int SPINNER_FIELD_WIDTH = 6; + public static final List DISPOSABLES = new ArrayList<>(); + + private final ConfigManager configManager; + + private ConfigDescriptor pluginConfig = null; + + private final TitleCaseListCellRenderer listCellRenderer = new TitleCaseListCellRenderer(); + + @Inject + ConfigPanel(ChinBreakHandlerPlugin chinBreakHandlerPlugin) + { + this.configManager = chinBreakHandlerPlugin.getConfigManager(); + + setBackground(ChinBreakHandlerPanel.PANEL_BACKGROUND_COLOR); + setBorder(new EmptyBorder(5, 10, 0, 10)); + setLayout(new DynamicGridLayout(0, 1, 0, 5)); + } + + public void init(Config config) + { + pluginConfig = getConfigDescriptor(config); + + try + { + configManager.setDefaultConfiguration(config, false); + } + catch (ThreadDeath e) + { + throw e; + } + catch (Throwable ex) + { + log.warn("Unable to reset plugin configuration", ex); + } + + rebuild(); + } + + private void rebuild() + { + removeAll(); + + final Map titleWidgets = new HashMap<>(); + final Map topLevelPanels = new TreeMap<>((a, b) -> + ComparisonChain.start() + .compare(a.position(), b.position()) + .compare(a.name(), b.name()) + .result()); + + for (ConfigSectionDescriptor ctd : pluginConfig.getSections()) + { + ConfigSection ct = ctd.getSection(); + final JPanel title = new JPanel(); + title.setLayout(new BoxLayout(title, BoxLayout.Y_AXIS)); + title.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); + + final JPanel sectionHeader = new JPanel(); + sectionHeader.setLayout(new BorderLayout()); + sectionHeader.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); + + title.add(sectionHeader, BorderLayout.NORTH); + + String name = ct.name(); + final JLabel sectionName = new JLabel(name); + sectionName.setForeground(ColorScheme.BRAND_ORANGE); + sectionName.setFont(FontManager.getRunescapeBoldFont()); + sectionName.setToolTipText("" + name + ":
" + ct.description() + ""); + sectionName.setBorder(new EmptyBorder(0, 0, 3, 1)); + sectionHeader.add(sectionName, BorderLayout.CENTER); + + final JPanel sectionContents = new JPanel(); + sectionContents.setLayout(new DynamicGridLayout(0, 1, 0, 5)); + sectionContents.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); + sectionContents.setBorder(new EmptyBorder(0, 5, 0, 0)); + title.add(sectionContents, BorderLayout.SOUTH); + + titleWidgets.put(ctd.getKey(), sectionContents); + + // would talk about adding a parent to itself idrk :( +// JPanel titleSection = titleWidgets.get(ct.name()); +// +// if (titleSection != null) +// { +// titleSection.add(title); +// } +// else +// { +// topLevelPanels.put(ctd, title); +// } + topLevelPanels.put(ctd, title); + } + + for (ConfigItemDescriptor cid : pluginConfig.getItems()) + { + if (!hideUnhide(pluginConfig, cid)) + { + continue; + } + + JPanel item = new JPanel(); + item.setLayout(new BorderLayout()); + item.setMinimumSize(new Dimension(PANEL_WIDTH, 0)); + String name = cid.getItem().name(); + + if (!name.isEmpty()) + { + JLabel configEntryName = new JLabel(name); + configEntryName.setForeground(Color.WHITE); + configEntryName.setToolTipText("" + name + ":
" + cid.getItem().description() + ""); + item.add(configEntryName, BorderLayout.CENTER); + } + + if (cid.getType() == boolean.class) + { + JCheckBox checkbox = new JCheckBox(); + checkbox.setBackground(ColorScheme.LIGHT_GRAY_COLOR); + checkbox.setSelected(Boolean.parseBoolean(configManager.getConfiguration(pluginConfig.getGroup().value(), cid.getItem().keyName()))); + checkbox.addActionListener(ae -> changeConfiguration(checkbox, pluginConfig, cid)); + + item.add(checkbox, BorderLayout.EAST); + } + + if (cid.getType() == int.class) + { + int value = Integer.parseInt(configManager.getConfiguration(pluginConfig.getGroup().value(), cid.getItem().keyName())); + + Units units = cid.getUnits(); + Range range = cid.getRange(); + int min = 0, max = Integer.MAX_VALUE; + if (range != null) + { + min = range.min(); + max = range.max(); + } + + // Config may previously have been out of range + value = Ints.constrainToRange(value, min, max); + + SpinnerModel model = new SpinnerNumberModel(value, min, max, 1); + JSpinner spinner = new JSpinner(model); + Component editor = spinner.getEditor(); + JFormattedTextField spinnerTextField = ((JSpinner.DefaultEditor) editor).getTextField(); + spinnerTextField.setColumns(SPINNER_FIELD_WIDTH); + spinner.addChangeListener(ce -> changeConfiguration(spinner, pluginConfig, cid)); + + if (units != null) + { + spinnerTextField.setFormatterFactory(new UnitFormatterFactory(units.value())); + } + + item.add(spinner, BorderLayout.EAST); + } + + if (cid.getType() instanceof Class && ((Class) cid.getType()).isEnum()) + { + Class type = (Class) cid.getType(); + + JComboBox box = new JComboBox>(type.getEnumConstants()); // NOPMD: UseDiamondOperator + // set renderer prior to calling box.getPreferredSize(), since it will invoke the renderer + // to build components for each combobox element in order to compute the display size of the + // combobox + box.setRenderer(listCellRenderer); + if (!name.isEmpty()) + { + box.setPreferredSize(new Dimension(box.getPreferredSize().width, 25)); + } + else + { + box.setPreferredSize(new Dimension(PANEL_WIDTH - 10, 25)); + } + box.setForeground(Color.WHITE); + box.setFocusable(false); + + try + { + Enum selectedItem = Enum.valueOf(type, configManager.getConfiguration(pluginConfig.getGroup().value(), cid.getItem().keyName())); + box.setSelectedItem(selectedItem); + box.setToolTipText(Text.titleCase(selectedItem)); + } + catch (IllegalArgumentException ex) + { + log.debug("invalid seleced item", ex); + } + box.addItemListener(e -> + { + if (e.getStateChange() == ItemEvent.SELECTED) + { + changeConfiguration(box, pluginConfig, cid); + box.setToolTipText(Text.titleCase((Enum) box.getSelectedItem())); + } + }); + item.add(box, BorderLayout.EAST); + } + + JPanel title = titleWidgets.get(cid.getItem().section()); + + if (title != null) + { + title.add(item); + } + else + { + topLevelPanels.put(cid, item); + } + } + + topLevelPanels.values().forEach(this::add); + + revalidate(); + repaint(); + } + + private void changeConfiguration(Component component, ConfigDescriptor cd, ConfigItemDescriptor cid) + { + if (component instanceof JCheckBox) + { + JCheckBox checkbox = (JCheckBox) component; + configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), "" + checkbox.isSelected()); + } + else if (component instanceof JSpinner) + { + JSpinner spinner = (JSpinner) component; + configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), "" + spinner.getValue()); + } + else if (component instanceof JComboBox) + { + JComboBox jComboBox = (JComboBox) component; + configManager.setConfiguration(cd.getGroup().value(), cid.getItem().keyName(), ((Enum) jComboBox.getSelectedItem()).name()); + } + + rebuild(); + } + + private boolean hideUnhide(ConfigDescriptor cd, ConfigItemDescriptor cid) + { +// boolean unhide = cid.getItem().hidden(); +// boolean hide = !cid.getItem().hide().isEmpty(); +// +// if (unhide || hide) +// { +// boolean show = false; +// +// List itemHide = Splitter +// .onPattern("\\|\\|") +// .trimResults() +// .omitEmptyStrings() +// .splitToList(String.format("%s || %s", cid.getItem().unhide(), cid.getItem().hide())); +// +// for (ConfigItemDescriptor cid2 : cd.getItems()) +// { +// if (itemHide.contains(cid2.getItem().keyName())) +// { +// if (cid2.getType() == boolean.class) +// { +// show = Boolean.parseBoolean(configManager.getConfiguration(cd.getGroup().value(), cid2.getItem().keyName())); +// } +// else if (cid2.getType() instanceof Class && ((Class) cid2.getType()).isEnum()) +// { +// Class type = (Class) cid2.getType(); +// try +// { +// Enum selectedItem = Enum.valueOf(type, configManager.getConfiguration(cd.getGroup().value(), cid2.getItem().keyName())); +// if (!cid.getItem().unhideValue().equals("")) +// { +// List unhideValue = Splitter +// .onPattern("\\|\\|") +// .trimResults() +// .omitEmptyStrings() +// .splitToList(cid.getItem().unhideValue()); +// +// show = unhideValue.contains(selectedItem.toString()); +// } +// else if (!cid.getItem().hideValue().equals("")) +// { +// List hideValue = Splitter +// .onPattern("\\|\\|") +// .trimResults() +// .omitEmptyStrings() +// .splitToList(cid.getItem().hideValue()); +// +// show = !hideValue.contains(selectedItem.toString()); +// } +// } +// catch (IllegalArgumentException ignored) +// { +// } +// } +// } +// } +// +// return (!unhide || show) && (!hide || !show); +// } + + return true; + } + + /** + * Does DFS on a class's interfaces to find all of its implemented fields. + */ + private Collection getAllDeclaredInterfaceFields(Class clazz) + { + Collection methods = new HashSet<>(); + Stack> interfaces = new Stack<>(); + interfaces.push(clazz); + + while (!interfaces.isEmpty()) + { + Class interfaze = interfaces.pop(); + Collections.addAll(methods, interfaze.getDeclaredFields()); + Collections.addAll(interfaces, interfaze.getInterfaces()); + } + + return methods; + } + + public ConfigDescriptor getConfigDescriptor(Config configurationProxy) + { + Class inter = configurationProxy.getClass().getInterfaces()[0]; + ConfigGroup group = inter.getAnnotation(ConfigGroup.class); + + if (group == null) + { + throw new IllegalArgumentException("Not a config group"); + } + + final List sections = getAllDeclaredInterfaceFields(inter).stream() + .filter(m -> m.isAnnotationPresent(ConfigSection.class) && m.getType() == String.class) + .map(m -> + { + try + { + return new ConfigSectionDescriptor( + String.valueOf(m.get(inter)), + m.getDeclaredAnnotation(ConfigSection.class) + ); + } + catch (IllegalAccessException e) + { + log.warn("Unable to load section {}::{}", inter.getSimpleName(), m.getName()); + return null; + } + }) + .filter(Objects::nonNull) + .sorted((a, b) -> ComparisonChain.start() + .compare(a.getSection().position(), b.getSection().position()) + .compare(a.getSection().name(), b.getSection().name()) + .result()) + .collect(Collectors.toList()); + + final List titles = getAllDeclaredInterfaceFields(inter).stream() + .filter(m -> m.isAnnotationPresent(ConfigSection.class) && m.getType() == String.class) + .map(m -> + { + try + { + return new ConfigSectionDescriptor( + String.valueOf(m.get(inter)), + m.getDeclaredAnnotation(ConfigSection.class) + ); + } + catch (IllegalAccessException e) + { + log.warn("Unable to load title {}::{}", inter.getSimpleName(), m.getName()); + return null; + } + }) + .filter(Objects::nonNull) + .sorted((a, b) -> ComparisonChain.start() + .compare(a.getSection().position(), b.getSection().position()) + .compare(a.getSection().name(), b.getSection().name()) + .result()) + .collect(Collectors.toList()); + + final List items = Arrays.stream(inter.getMethods()) + .filter(m -> m.getParameterCount() == 0 && m.isAnnotationPresent(ConfigItem.class)) + .map(m -> new ConfigItemDescriptor( + m.getDeclaredAnnotation(ConfigItem.class), + m.getReturnType(), + m.getDeclaredAnnotation(Range.class), + m.getDeclaredAnnotation(Alpha.class), + m.getDeclaredAnnotation(Units.class) + )) + .sorted((a, b) -> ComparisonChain.start() + .compare(a.getItem().position(), b.getItem().position()) + .compare(a.getItem().name(), b.getItem().name()) + .result()) + .collect(Collectors.toList()); + + return new ConfigDescriptor(group, sections, items); + } +} \ No newline at end of file diff --git a/src/main/java/ChinBreakHandler/util/DeferredDocumentChangedListener.java b/src/main/java/ChinBreakHandler/util/DeferredDocumentChangedListener.java new file mode 100644 index 00000000..c6606289 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/DeferredDocumentChangedListener.java @@ -0,0 +1,57 @@ +package ChinBreakHandler.util; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.util.ArrayList; +import java.util.List; + +public class DeferredDocumentChangedListener implements DocumentListener +{ + private final Timer timer; + private final List listeners; + + public DeferredDocumentChangedListener() + { + listeners = new ArrayList<>(25); + timer = new Timer(350, e -> fireStateChanged()); + timer.setRepeats(false); + } + + public void addChangeListener(ChangeListener listener) + { + listeners.add(listener); + } + + private void fireStateChanged() + { + if (!listeners.isEmpty()) + { + ChangeEvent evt = new ChangeEvent(this); + for (ChangeListener listener : listeners) + { + listener.stateChanged(evt); + } + } + } + + @Override + public void insertUpdate(DocumentEvent e) + { + timer.restart(); + } + + @Override + public void removeUpdate(DocumentEvent e) + { + timer.restart(); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + timer.restart(); + } +} \ No newline at end of file diff --git a/src/main/java/ChinBreakHandler/util/FixedWidthPanel.java b/src/main/java/ChinBreakHandler/util/FixedWidthPanel.java new file mode 100644 index 00000000..2bd176f2 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/FixedWidthPanel.java @@ -0,0 +1,15 @@ +package ChinBreakHandler.util; + +import net.runelite.client.ui.PluginPanel; + +import javax.swing.*; +import java.awt.*; + +public class FixedWidthPanel extends JPanel +{ + @Override + public Dimension getPreferredSize() + { + return new Dimension(PluginPanel.PANEL_WIDTH, super.getPreferredSize().height); + } +} \ No newline at end of file diff --git a/src/main/java/ChinBreakHandler/util/IntRandomNumberGenerator.java b/src/main/java/ChinBreakHandler/util/IntRandomNumberGenerator.java new file mode 100644 index 00000000..f1908aa4 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/IntRandomNumberGenerator.java @@ -0,0 +1,31 @@ +package ChinBreakHandler.util; + +import java.util.PrimitiveIterator; +import java.util.Random; + +public final class IntRandomNumberGenerator +{ + private final PrimitiveIterator.OfInt randomIterator; + + /** + * Initialize a new random number generator that generates + * random numbers in the range [min, max] + * + * @param min - the min value (inclusive) + * @param max - the max value (inclusive) + */ + public IntRandomNumberGenerator(int min, int max) + { + randomIterator = new Random().ints(min, max + 1).iterator(); + } + + /** + * Returns a random number in the range (min, max) + * + * @return a random number in the range (min, max) + */ + public int nextInt() + { + return randomIterator.nextInt(); + } +} diff --git a/src/main/java/ChinBreakHandler/util/JMultilineLabel.java b/src/main/java/ChinBreakHandler/util/JMultilineLabel.java new file mode 100644 index 00000000..6066f5e7 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/JMultilineLabel.java @@ -0,0 +1,30 @@ +package ChinBreakHandler.util; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.text.DefaultCaret; + +public class JMultilineLabel extends JTextArea +{ + private static final long serialVersionUID = 1L; + + public JMultilineLabel() + { + super(); + + setEditable(false); + setCursor(null); + setOpaque(false); + setFocusable(false); + setWrapStyleWord(true); + setLineWrap(true); + setBorder(new EmptyBorder(0, 8, 0, 8)); + setAlignmentY(JLabel.CENTER_ALIGNMENT); + + DefaultCaret caret = (DefaultCaret) getCaret(); + if (caret != null) + { + caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + } + } +} diff --git a/src/main/java/ChinBreakHandler/util/MacOSPopupFactory.java b/src/main/java/ChinBreakHandler/util/MacOSPopupFactory.java new file mode 100644 index 00000000..507c1550 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/MacOSPopupFactory.java @@ -0,0 +1,13 @@ +package ChinBreakHandler.util; + +import javax.swing.*; +import java.awt.*; + +class MacOSPopupFactory extends PopupFactory +{ + @Override + protected Popup getPopup(Component owner, Component contents, int x, int y, boolean isHeavyWeightPopup) throws IllegalArgumentException + { + return super.getPopup(owner, contents, x, y, true); + } +} diff --git a/src/main/java/ChinBreakHandler/util/OnOffToggleButton.java b/src/main/java/ChinBreakHandler/util/OnOffToggleButton.java new file mode 100644 index 00000000..cd107272 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/OnOffToggleButton.java @@ -0,0 +1,39 @@ +package ChinBreakHandler.util; + +import ChinBreakHandler.ChinBreakHandlerPlugin; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.SwingUtil; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class OnOffToggleButton extends JToggleButton +{ + private static final ImageIcon ON_SWITCHER; + private static final ImageIcon OFF_SWITCHER; + + static + { + BufferedImage onSwitcher = ImageUtil.loadImageResource(ChinBreakHandlerPlugin.class, "/switcher_on.png"); + ON_SWITCHER = new ImageIcon(ImageUtil.recolorImage(onSwitcher, ColorScheme.BRAND_ORANGE)); + OFF_SWITCHER = new ImageIcon(ImageUtil.flipImage( + ImageUtil.luminanceScale( + ImageUtil.grayscaleImage(onSwitcher), + 0.61f + ), + true, + false + )); + } + + public OnOffToggleButton() + { + super(OFF_SWITCHER); + setSelectedIcon(ON_SWITCHER); + SwingUtil.removeButtonDecorations(this); + setPreferredSize(new Dimension(25, 0)); + SwingUtil.addModalTooltip(this, "Disable", "Enable"); + } +} diff --git a/src/main/java/ChinBreakHandler/util/ProfilesData.java b/src/main/java/ChinBreakHandler/util/ProfilesData.java new file mode 100644 index 00000000..1b23cc37 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/ProfilesData.java @@ -0,0 +1,80 @@ +package ChinBreakHandler.util; + +import net.runelite.client.config.ConfigManager; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Base64; + +public class ProfilesData +{ + private static final int iterations = 100000; + + private static String decryptText(byte[] enc, SecretKey aesKey) throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchPaddingException + { + Cipher cipher = Cipher.getInstance("AES"); + SecretKeySpec newKey = new SecretKeySpec(aesKey.getEncoded(), "AES"); + cipher.init(Cipher.DECRYPT_MODE, newKey); + return new String(cipher.doFinal(enc)); + } + + public static String getProfileData(ConfigManager configManager, char[] password) throws InvalidKeySpecException, NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchPaddingException + { + String tmp = configManager.getConfiguration("piggyProfiles", "profilesData"); + if (tmp.startsWith("¬")) + { + tmp = tmp.substring(2); + return decryptText(base64Decode(tmp), getAesKey(configManager, password)).trim(); + } + return tmp; + } + + private static byte[] getSalt(ConfigManager configManager) + { + String salt = configManager.getConfiguration("piggyProfiles", "salt"); + if (salt.length() == 0) + { + return new byte[0]; + } + return base64Decode(salt); + } + + private static SecretKey getAesKey(ConfigManager configManager, char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException + { + if (getSalt(configManager).length == 0) + { + byte[] b = new byte[16]; + SecureRandom.getInstanceStrong().nextBytes(b); + setSalt(b, configManager); + } + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(password, getSalt(configManager), iterations, 128); + return factory.generateSecret(spec); + } + + private static void setSalt(byte[] bytes, ConfigManager configManager) + { + configManager.setConfiguration("piggyProfiles", "salt", base64Encode(bytes)); + } + + private static byte[] base64Decode(String data) + { + return Base64.getDecoder().decode(data); + } + + private static String base64Encode(byte[] data) + { + return Base64.getEncoder().encodeToString(data); + } +} diff --git a/src/main/java/ChinBreakHandler/util/SwingUtilExtended.java b/src/main/java/ChinBreakHandler/util/SwingUtilExtended.java new file mode 100644 index 00000000..b20da346 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/SwingUtilExtended.java @@ -0,0 +1,297 @@ +package ChinBreakHandler.util; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.OSType; +import net.runelite.client.util.OSXUtil; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.swing.*; +import javax.swing.plaf.FontUIResource; +import javax.swing.plaf.SliderUI; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.lang.reflect.InvocationTargetException; +import java.util.Enumeration; +import java.util.function.BiConsumer; + +/** + * Various Swing utilities. + */ +@Slf4j +public class SwingUtilExtended +{ + /** + * Sets some sensible defaults for swing. + * IMPORTANT! Needs to be called before main frame creation + */ + public static void setupDefaults() + { + // Force heavy-weight popups/tooltips. + // Prevents them from being obscured by the game applet. + ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false); + ToolTipManager.sharedInstance().setInitialDelay(300); + JPopupMenu.setDefaultLightWeightPopupEnabled(false); + + UIManager.put("Button.foreground", Color.WHITE); + UIManager.put("MenuItem.foreground", Color.WHITE); + UIManager.put("Panel.background", ColorScheme.DARK_GRAY_COLOR); + UIManager.put("TextField.selectionBackground", ColorScheme.BRAND_ORANGE_TRANSPARENT); + UIManager.put("TextField.selectionForeground", Color.WHITE); + UIManager.put("FormattedTextField.selectionBackground", ColorScheme.BRAND_ORANGE_TRANSPARENT); + UIManager.put("FormattedTextField.selectionForeground", Color.WHITE); + UIManager.put("TextArea.selectionBackground", ColorScheme.BRAND_ORANGE_TRANSPARENT); + UIManager.put("TextArea.selectionForeground", Color.WHITE); + UIManager.put("SliderUI", SliderUI.class.getName()); + + // Do not render shadows under popups/tooltips. + // Fixes black boxes under popups that are above the game applet. + System.setProperty("jgoodies.popupDropShadowEnabled", "false"); + + // Do not fill in background on repaint. Reduces flickering when + // the applet is resized. + System.setProperty("sun.awt.noerasebackground", "true"); + } + + /** + * Safely sets Swing theme + * + * @param laf the swing look and feel + */ + public static void setTheme(@Nonnull final LookAndFeel laf) + { + try + { + UIManager.setLookAndFeel(laf); + + if (OSType.getOSType() == OSType.MacOS) + { + // On MacOS Substance doesn't install its own popup factory, and the default one uses lightweight + // components unless the Aqua LAF is used. Lightweight components do not render correctly over AWT + // canvases on MacOS - so replace the popup factory one with that forces heavy components. + PopupFactory.setSharedInstance(new MacOSPopupFactory()); + } + } + catch (UnsupportedLookAndFeelException ex) + { + log.warn("Unable to set look and feel", ex); + } + } + + /** + * Sets default Swing font. + * IMPORTANT! Needs to be called before main frame creation + * + * @param font the new font to use + */ + public static void setFont(@Nonnull final Font font) + { + final FontUIResource f = new FontUIResource(font); + final Enumeration keys = UIManager.getDefaults().keys(); + + while (keys.hasMoreElements()) + { + final Object key = keys.nextElement(); + final Object value = UIManager.get(key); + + if (value instanceof FontUIResource) + { + UIManager.put(key, f); + } + } + } + + /** + * Create tray icon. + * + * @param icon the icon + * @param title the title + * @param frame the frame + * @return the tray icon + */ + @Nullable + public static TrayIcon createTrayIcon(@Nonnull final Image icon, @Nonnull final String title, @Nonnull final Frame frame) + { + if (!SystemTray.isSupported()) + { + return null; + } + + final SystemTray systemTray = SystemTray.getSystemTray(); + final TrayIcon trayIcon = new TrayIcon(icon, title); + trayIcon.setImageAutoSize(true); + + try + { + systemTray.add(trayIcon); + } + catch (AWTException ex) + { + log.debug("Unable to add system tray icon", ex); + return trayIcon; + } + + // Bring to front when tray icon is clicked + trayIcon.addMouseListener(new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent e) + { + if (OSType.getOSType() == OSType.MacOS && !frame.isFocused()) + { + // On macOS, frame.setVisible(true) only restores focus when the visibility was previously false. + // The frame's visibility is not set to false when the window loses focus, so we set it manually. + // Additionally, in order to bring the window to the foreground, + // frame.setVisible(true) calls CPlatformWindow::nativePushNSWindowToFront. + // However, this native method is not called with activateIgnoringOtherApps:YES, + // so any other active window will prevent our window from being brought to the front. + // To work around this, we use our macOS-specific requestForeground(). + frame.setVisible(false); + OSXUtil.requestForeground(); + } + frame.setVisible(true); + frame.setState(Frame.NORMAL); // Restore + } + }); + + return trayIcon; + } + + /** + * Create swing button from navigation button. + * + * @param navigationButton the navigation button + * @param iconSize the icon size (in case it is 0 default icon size will be used) + * @param specialCallback the special callback + * @return the swing button + */ + public static JButton createSwingButton( + @Nonnull final NavigationButton navigationButton, + int iconSize, + @Nullable final BiConsumer specialCallback) + { + + final BufferedImage scaledImage = iconSize > 0 + ? ImageUtil.resizeImage(navigationButton.getIcon(), iconSize, iconSize) + : navigationButton.getIcon(); + + final JButton button = new JButton(); + button.setSize(scaledImage.getWidth(), scaledImage.getHeight()); + button.setToolTipText(navigationButton.getTooltip()); + button.setIcon(new ImageIcon(scaledImage)); + button.setFocusable(false); + button.addActionListener(e -> + { + if (specialCallback != null) + { + specialCallback.accept(navigationButton, button); + } + + if (navigationButton.getOnClick() != null) + { + navigationButton.getOnClick().run(); + } + }); + + if (navigationButton.getPopup() != null) + { + final JPopupMenu popupMenu = new JPopupMenu(); + + navigationButton.getPopup().forEach((name, callback) -> + { + final JMenuItem menuItem = new JMenuItem(name); + menuItem.addActionListener((e) -> callback.run()); + popupMenu.add(menuItem); + }); + + button.setComponentPopupMenu(popupMenu); + } + + return button; + } + + public static void removeButtonDecorations(AbstractButton button) + { + button.setBorderPainted(false); + button.setContentAreaFilled(false); + button.setFocusPainted(false); + button.setMargin(new Insets(0, 0, 0, 0)); + button.setOpaque(false); + } + + public static void addModalTooltip(AbstractButton button, String on, String off) + { + button.setToolTipText(button.isSelected() ? on : off); + button.addItemListener(l -> button.setToolTipText(button.isSelected() ? on : off)); + } + + /** + * Removes all of a component's children faster than calling removeAll() on it in many cases + */ + public static void fastRemoveAll(Container c) + { + // If we are not on the EDT this will deadlock, in addition to being totally unsafe + assert SwingUtilities.isEventDispatchThread(); + + // when a component is removed it has to be resized for some reason, but only if it's valid + // so we make sure to invalidate everything before removing it + c.invalidate(); + for (int i = 0; i < c.getComponentCount(); i++) + { + Component ic = c.getComponent(i); + + // removeAll and removeNotify are both recursive, so we have to recurse before them + if (ic instanceof Container) + { + fastRemoveAll((Container) ic); + } + + // each removeNotify needs to remove anything from the event queue that is for that widget + // this however requires taking a lock, and is moderately slow, so we just execute all of + // those events with a secondary event loop + pumpPendingEvents(); + + // call removeNotify early; this is most of the work in removeAll, and generates events that + // the next secondaryLoop will pickup + ic.removeNotify(); + } + + // Actually remove anything + c.removeAll(); + } + + /** + * Run any events currently in the event queue + */ + public static void pumpPendingEvents() + { + EventQueue eq = Toolkit.getDefaultToolkit().getSystemEventQueue(); + + if (eq.peekEvent() != null) + { + SecondaryLoop l = eq.createSecondaryLoop(); + SwingUtilities.invokeLater(l::exit); + l.enter(); + } + } + + /** + * Executes a runnable on the EDT, blocking until it finishes. + */ + public static void syncExec(final Runnable r) throws InvocationTargetException, InterruptedException + { + if (EventQueue.isDispatchThread()) + { + r.run(); + } + else + { + EventQueue.invokeAndWait(r); + } + } +} diff --git a/src/main/java/ChinBreakHandler/util/UnitFormatter.java b/src/main/java/ChinBreakHandler/util/UnitFormatter.java new file mode 100644 index 00000000..7b75a961 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/UnitFormatter.java @@ -0,0 +1,69 @@ +package ChinBreakHandler.util; + +/* + * Copyright (c) 2020, Hydrox6 + * 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 OWNER 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. + */ +import javax.swing.*; +import java.text.ParseException; + +final class UnitFormatter extends JFormattedTextField.AbstractFormatter +{ + private final String units; + + UnitFormatter(String units) + { + this.units = units; + } + + @Override + public Object stringToValue(final String text) throws ParseException + { + final String trimmedText; + + // Using the spinner controls causes the value to have the unit on the end, so remove it + if (text.endsWith(units)) + { + trimmedText = text.substring(0, text.length() - units.length()); + } + else + { + trimmedText = text; + } + + try + { + return Integer.valueOf(trimmedText); + } + catch (NumberFormatException e) + { + throw new ParseException(trimmedText + " is not an integer.", 0); + } + } + + @Override + public String valueToString(final Object value) + { + return value + units; + } +} diff --git a/src/main/java/ChinBreakHandler/util/UnitFormatterFactory.java b/src/main/java/ChinBreakHandler/util/UnitFormatterFactory.java new file mode 100644 index 00000000..fcb29769 --- /dev/null +++ b/src/main/java/ChinBreakHandler/util/UnitFormatterFactory.java @@ -0,0 +1,44 @@ +package ChinBreakHandler.util; +/* + * Copyright (c) 2020, Hydrox6 + * 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 OWNER 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. + */ + +import lombok.RequiredArgsConstructor; + +import javax.swing.*; +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +public final class UnitFormatterFactory extends JFormattedTextField.AbstractFormatterFactory +{ + private final String units; + private final Map formatters = new HashMap<>(); + + @Override + public JFormattedTextField.AbstractFormatter getFormatter(final JFormattedTextField tf) + { + return formatters.computeIfAbsent(tf, (key) -> new UnitFormatter(units)); + } +} diff --git a/src/main/java/com/example/ExampleConfig.java b/src/main/java/com/example/ExampleConfig.java deleted file mode 100644 index f9ba3d8c..00000000 --- a/src/main/java/com/example/ExampleConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example; - -import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; -import net.runelite.client.config.ConfigItem; - -@ConfigGroup("example") -public interface ExampleConfig extends Config -{ - @ConfigItem( - keyName = "greeting", - name = "Welcome Greeting", - description = "The message to show to the user when they login" - ) - default String greeting() - { - return "Hello"; - } -} diff --git a/src/main/java/com/example/ExamplePlugin.java b/src/main/java/com/example/ExamplePlugin.java deleted file mode 100644 index ee27aebe..00000000 --- a/src/main/java/com/example/ExamplePlugin.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example; - -import com.google.inject.Provides; -import javax.inject.Inject; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.ChatMessageType; -import net.runelite.api.Client; -import net.runelite.api.GameState; -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; - -@Slf4j -@PluginDescriptor( - name = "Example" -) -public class ExamplePlugin extends Plugin -{ - @Inject - private Client client; - - @Inject - private ExampleConfig config; - - @Override - protected void startUp() throws Exception - { - log.info("Example started!"); - } - - @Override - protected void shutDown() throws Exception - { - log.info("Example stopped!"); - } - - @Subscribe - public void onGameStateChanged(GameStateChanged gameStateChanged) - { - if (gameStateChanged.getGameState() == GameState.LOGGED_IN) - { - client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", "Example says " + config.greeting(), null); - } - } - - @Provides - ExampleConfig provideConfig(ConfigManager configManager) - { - return configManager.getConfig(ExampleConfig.class); - } -} diff --git a/src/main/java/com/example/GemCrabFighter/GemCrabFighterConfig.java b/src/main/java/com/example/GemCrabFighter/GemCrabFighterConfig.java new file mode 100644 index 00000000..5c4578be --- /dev/null +++ b/src/main/java/com/example/GemCrabFighter/GemCrabFighterConfig.java @@ -0,0 +1,61 @@ +package com.example.GemCrabFighter; + +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Range; + +@ConfigGroup("GemCrabFighter") +public interface GemCrabFighterConfig extends Config { + + @ConfigItem(keyName = "trainMelee", name = "Train Melee", description = "Whether to train melee or not.", position = 91, section = "damageConfig") + default boolean trainMelee() + { + return true; + } + + @ConfigItem(keyName = "trainRange", name = "Train Range", description = "Whether to train range or not.", position = 91, section = "damageConfig") + default boolean trainRange() + { + return false; + } + + @ConfigItem(keyName="trainMagic", name="Train Magic", description="Whether to train magic or not.", position=91, section="damageConfig") + default boolean trainMagic() + { + return false; + } + + @ConfigItem(keyName = "mainId", name = "Main Weapon ID", description = "Main weapon ID to use.", position = 92, section = "damageConfig") + default int mainId() + { + return 22978; + } + + @ConfigItem(keyName = "specId", name = "Spec Weapon ID", description = "Spec weapon ID to use.", position = 91, section = "damageConfig") + default int specId() + { + return 13576; + } + + @Range(min = 0, max = 100) + @ConfigItem(keyName = "specTreshhold", name = "Spec threshhold", description = "Amount of spec % before using spec.", position = 96, section = "damageConfig") + default int specTreshhold() + { + return 70; + } + + @ConfigItem(keyName = "startButton", name = "Start/Stop", description = "Button to start or stop the plugin", position = 33) + default boolean startButton() + { + return false; + } + + @ConfigItem(keyName = "debugMode", name = "Debug", description = "Button to show debug message", position = 34) + default boolean debugMode() + { + return true; + } +} diff --git a/src/main/java/com/example/GemCrabFighter/GemCrabFighterOverlay.java b/src/main/java/com/example/GemCrabFighter/GemCrabFighterOverlay.java new file mode 100644 index 00000000..de22a863 --- /dev/null +++ b/src/main/java/com/example/GemCrabFighter/GemCrabFighterOverlay.java @@ -0,0 +1,132 @@ +package com.example.GemCrabFighter; + +import com.google.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.api.VarPlayer; +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 java.awt.*; + +public class GemCrabFighterOverlay extends OverlayPanel { + + private final Client client; + private final GemCrabFighterPlugin plugin; + private final GemCrabFighterConfig config; + + @Inject + private GemCrabFighterOverlay(Client client, GemCrabFighterPlugin plugin, GemCrabFighterConfig config) { + this.client = client; + this.plugin = plugin; + this.config = config; + setPosition(OverlayPosition.TOP_LEFT); + } + + @Override + public Dimension render(Graphics2D graphics) { + if (!plugin.isPluginStarted()) { + return null; + } + + // Title + panelComponent.getChildren().add(TitleComponent.builder() + .text("Gem Crab Fighter") + .color(Color.GREEN) + .build()); + + // Status + panelComponent.getChildren().add(LineComponent.builder() + .left("Status:") + .right(plugin.isPluginStarted() ? "Running" : "Stopped") + .rightColor(plugin.isPluginStarted() ? Color.GREEN : Color.RED) + .build()); + + // Current State + panelComponent.getChildren().add(LineComponent.builder() + .left("State:") + .right(plugin.getCurrentState().toString()) + .rightColor(getStateColor(plugin.getCurrentState())) + .build()); + + // Combat Style + panelComponent.getChildren().add(LineComponent.builder() + .left("Combat:") + .right(config.combatStyle()) + .build()); + + // Health + int currentHp = client.getBoostedSkillLevel(Skill.HITPOINTS); + int maxHp = client.getRealSkillLevel(Skill.HITPOINTS); + panelComponent.getChildren().add(LineComponent.builder() + .left("HP:") + .right(currentHp + "/" + maxHp) + .rightColor(getHealthColor(currentHp, maxHp)) + .build()); + + // Prayer + if (config.usePrayer()) { + int currentPrayer = client.getBoostedSkillLevel(Skill.PRAYER); + int maxPrayer = client.getRealSkillLevel(Skill.PRAYER); + panelComponent.getChildren().add(LineComponent.builder() + .left("Prayer:") + .right(currentPrayer + "/" + maxPrayer) + .rightColor(getPrayerColor(currentPrayer, maxPrayer)) + .build()); + } + + // Special Attack + if (config.useSpecWeapon()) { + int specEnergy = client.getVarpValue(VarPlayer.SPECIAL_ATTACK_PERCENT) / 10; + panelComponent.getChildren().add(LineComponent.builder() + .left("Spec:") + .right(specEnergy + "%") + .rightColor(specEnergy >= config.specEnergyThreshold() ? Color.GREEN : Color.ORANGE) + .build()); + } + + return super.render(graphics); + } + + private Color getStateColor(State state) { + switch (state) { + case FIGHTING: + return Color.RED; + case FIND_CRAB: + return Color.YELLOW; + case SWAP_CAVE: + return Color.ORANGE; + case EQUIP_GEAR: + return Color.CYAN; + case USE_SPEC: + return Color.MAGENTA; + case IDLE: + default: + return Color.WHITE; + } + } + + private Color getHealthColor(int current, int max) { + double percentage = (double) current / max * 100; + if (percentage <= 30) { + return Color.RED; + } else if (percentage <= 60) { + return Color.ORANGE; + } else { + return Color.GREEN; + } + } + + private Color getPrayerColor(int current, int max) { + double percentage = (double) current / max * 100; + if (percentage <= 20) { + return Color.RED; + } else if (percentage <= 50) { + return Color.ORANGE; + } else { + return Color.CYAN; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/GemCrabFighter/GemCrabFighterPlugin.java b/src/main/java/com/example/GemCrabFighter/GemCrabFighterPlugin.java new file mode 100644 index 00000000..b9833889 --- /dev/null +++ b/src/main/java/com/example/GemCrabFighter/GemCrabFighterPlugin.java @@ -0,0 +1,352 @@ +package com.example.GemCrabFighter; + +import com.example.GemCrabFighter.data.*; +import com.example.PiggyUtils.API.*; +import com.example.PacketUtils.WidgetInfoExtended; +import com.google.inject.Inject; +import com.google.inject.Provides; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.api.gameval.ItemID; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDependency; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.config.ConfigManager; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import lombok.extern.slf4j.Slf4j; +import com.example.EthanApiPlugin.EthanApiPlugin; + +import javax.net.ssl.KeyManager; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + + +@PluginDescriptor( + name = "Gem Crab Fighter", + description = "Automatically fights the Gemrock crab with optional combat style switching", + tags = {"KungKnudas", "GemCrab", "ethan"} +) +@Slf4j +public class GemCrabFighterPlugin extends Plugin { + public static Player player; + public static NPC currentNPC; + public static WorldPoint deathLocation; + + public static List inventorySetup = new ArrayList<>(); + + @Getter + private State state; + @Getter + private SubState subState; + + // booleans + @Getter + private boolean started; + private boolean deposited = false; + + // timers + private Instant timer; + private int timeout; + + @Inject + private Client client; + @Inject + private ClientThread clientThread; + @Inject + private GemCrabFighterConfig config; + @Inject + private GemCrabFighterOverlay overlay; + @Inject + private KeyManager keyManager; + + @Inject + public PlayerUtil playerUtil; + + @Override + protected void startUp() throws Exception { + log.info("Gemcrab Fighter started!"); + } + + @Override + protected void shutDown() throws Exception { + log.info("Gemcrab Fighter stopped!"); + } + + @Provides + private GemCrabFighterConfig getConfig(ConfigManager configManager) { + return configManager.getConfig(GemCrabFighterConfig.class); + } + + @Subscribe + private void onGameTick(GameTick event) { + if (!EthanApiPlugin.loggedIn() || !started) { + // We do an early return if the user isn't logged in\ + return; + } + if (!playerUtil.isAutoRetaliating()) { + EthanApiPlugin.sendClientMessage("[GemCrabAIO]TURN ON AUTO RETALIATE"); + EthanApiPlugin.stopPlugin(this); + return; + } + player = client.getLocalPlayer(); + if (player == null || !started) { + return; + } + if (client.getWidget(WidgetInfo.BANK_PIN_CONTAINER) != null) { + log.info("Enter bank pin manually"); + return; + } + state = getCurrentState(); + subState = getCurrentSubState(); + switch (state) { + case TIMEOUT: + timeout--; + return; + case LOGOUT: + EthanApiPlugin.sendClientMessage("We are missing a teleport to house tab, stopping plugin."); + resetPlugin(); + return; + case ANIMATING: + return; + } + switch (subState) { + case DRINK_POTIONS: + drinkPotions(); + timeout = tickDelay(); + break; + case EAT_FOOD: + eatFood(); + break; + case EQUIP_GEAR: + equipGear(); + break; + case USE_SPECIAL: + useSpec(); + break; + case TELE_GE: + teleportToGe(); + break; + case TELE_CIVITAS: + teleportCivitas(); + break; + case ATTACK_CRAB: + attackCrab(); + break; + case MINE_CRAB: + mineCrab(); + break; + case MOVE_DOWNSTAIRS: + walkDownstairs(); + break; + case OPEN_DOOR: + enterDownstairs(); + break; + case WALK_DOOR: + walkDoor(); + break; + case ENTER_LAIR: + enterLair(); + break; + case FIND_BANK: + interactWithBank(); + break; + case DEPOSIT: + depositItems(); + break; + case WITHDRAW: + withdrawItems(); + break; + } + } + + @Subscribe + private void onConfigChanged(ConfigChanged e) { + if (e.getGroup().equals("GemCrabFighter") && e.getKey().equals("startButton")) { + toggle(); + } + } + + // Utils + public State getCurrentState() { + if (timeout > 0) { + return State.TIMEOUT; + } + + if (EthanApiPlugin.isMoving()) { + timeout = tickDelay(); + return State.ANIMATING; + } + + if(shouldBank() && !InventoryUtil.hasItems("Fire Rune", "Earth Rune", "Law Rune") || InventoryUtil.hasItem(ItemID.RUNE_PICKAXE)) { + return State.LOGOUT; + } + + if(inCrabArea() && !shouldBank()) { + return State.FIND_CRAB; + } + } + } + + private void resetPlugin() { + started = false; + overlayManager.remove(runeDragonsOverlay); + overlayManager.remove(runeDragonsTileOverlay); + inventorySetup.clear(); + itemsToLoot.clear(); + timer = null; + currentNPC = null; + timeout = 0; + deposited = false; + + } + + protected int tickDelay() { + return MathUtil.random(config.tickDelayMin(), config.tickDelayMax()); + } + + @Subscribe + private void onConfigChanged(ConfigChanged e) { + if (e.getGroup().equals("RuneDragons") && e.getKey().equals("startButton")) { + toggle(); + } + } + + @Subscribe + private void onActorDeath(ActorDeath event) { + if (!started) { + return; + } + if (event.getActor() == currentNPC) { + deathLocation = event.getActor().getWorldLocation(); + log.debug("Our npc died, updating deathLocation: {}", deathLocation.toString()); + currentNPC = null; + killCount++; + } + } + + @Subscribe + private void onItemSpawned(ItemSpawned event) { + if (!started || !inDragons()) { + return; + } + if (lootableItem(event)) { + log.debug("Adding loot item: {}", client.getItemDefinition(event.getItem().getId()).getName()); + itemsToLoot.add(event); + } + } + + @Subscribe + private void onItemDespawned(ItemDespawned itemDespawned) { + if (!started || !inDragons()) { + return; + } + itemsToLoot.removeIf(itemSpawned -> itemSpawned.getItem().getId() == itemDespawned.getItem().getId()); + } + + @Subscribe + public void onNpcSpawned(NpcSpawned npcSpawned) + { + NPC npc = npcSpawned.getNpc(); + + if (npc.getId() == Constants.GEMSTONE_CRAB_ID) + { + log.debug("Gemstone Crab boss spawned"); + bossPresent = true; + notificationSent = false; + + + resetTunnel(); + // Start a new DPS tracking session + // This is where we reset stats - when a new boss spawns + resetDpsTracking(); + isTop16Damager = false; + fightInProgress = true; + fightStartTime = System.currentTimeMillis(); + setLastMiningAttempt(); + log.debug("New boss spawned, resetting DPS stats"); + } + // Track crab shells in the scene + else if (npc.getId() == Constants.GEMSTONE_CRAB_SHELL_ID) + { + WorldPoint location = npc.getWorldLocation(); + shells.put(location, npc); + log.debug("Crab shell NPC spawned at {}", location); + + } + } + + @Subscribe + public void onNpcDespawned(NpcDespawned npcDespawned) + { + NPC npc = npcDespawned.getNpc(); + + if (npc.getId() == Constants.GEMSTONE_CRAB_ID) + { + log.debug("Gemstone Crab boss despawned"); + bossPresent = false; + + // When the boss dies, highlight the nearest tunnel + if (config.highlightTunnel()) + { + findNearestTunnel(); + shouldHighlightTunnel = true; + log.debug("Boss died, highlighting nearest tunnel"); + } + + // Also find and highlight the shell in red by default + updateCrabShell(); + // Set to true but not top 16 yet (will be red) + shouldHighlightShell = true; + + log.debug("Boss died, highlighting shell in red"); + + } + else if (npc.getId() == Constants.GEMSTONE_CRAB_SHELL_ID) + { + // If the despawned NPC is our tracked shell, clear it + if (crabShell != null && npc.equals(crabShell)) + { + crabShell = null; + } + // Also remove from our shell map + shells.remove(npc.getWorldLocation()); + mineCountShell = 0; + } + } + + /** + * Check if the player is within any of the three Gemstone Crab areas + * @return true if player is in any of the three areas + */ + public boolean isPlayerInGemstoneArea() + { + if (client == null || client.getLocalPlayer() == null) + { + return false; + } + + // Get player's world location + WorldPoint playerLocation = client.getLocalPlayer().getWorldLocation(); + if (playerLocation == null) + { + return false; + } + + // Checks if player is close enough to any of the crabs + return playerLocation.distanceTo2D(Constants.EAST_CRAB) <= Constants.DISTANCE_THRESHOLD + || playerLocation.distanceTo2D(Constants.SOUTH_CRAB) <= Constants.DISTANCE_THRESHOLD + || playerLocation.distanceTo2D(Constants.NORTH_CRAB) <= Constants.DISTANCE_THRESHOLD; + } + +} diff --git a/src/main/java/com/example/GemCrabFighter/data/Constants.java b/src/main/java/com/example/GemCrabFighter/data/Constants.java new file mode 100644 index 00000000..12fef187 --- /dev/null +++ b/src/main/java/com/example/GemCrabFighter/data/Constants.java @@ -0,0 +1,95 @@ +package com.example.GemCrabFighter.data; + +import net.runelite.api.ItemID; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class Constants { + //Locations + public static final List HOME_REGIONS = Arrays.asList(7513, 7514, 7769, 7770); + public static final List LITH_REGIONS = Arrays.asList(14242, 6223); + public static final WorldArea EDGEVILLE_TELE = new WorldArea(3083, 3487, 17, 14, 0); + public static final WorldArea LITH_TELE = new WorldArea(3540, 10443, 21, 26, 0); + public static final WorldArea LITH_TELE_DOWNSTAIRS = new WorldArea(3542, 10467, 16, 19, 0); + public static final WorldArea RUNE_DRAGONS_DOOR = new WorldArea(1562, 5058, 12, 21, 0); + public static final WorldArea RUNE_DRAGONS_DOOR_ENTER = new WorldArea(1570, 5072, 4, 6, 0); + public static final WorldPoint RUNE_DRAGONS_DOOR_TILE = new WorldPoint(1572, 5074, 0); + public static final WorldArea RUNE_DRAGONS = new WorldArea(1574, 5061, 25, 28, 0); + + // Gemstone Crab boss NPC ID + public static final int GEMSTONE_CRAB_ID = 14779; + + // Gemstone Crab shell NPC ID + public static final int GEMSTONE_CRAB_SHELL_ID = 14780; + public static final int TUNNEL_OBJECT_ID = 57631; + + // HP widget ID constants + public static final int BOSS_HP_BAR_WIDGET_ID = 19857428; + + // Maximum distance to highlight tunnel (in tiles) + public static final int MAX_TUNNEL_DISTANCE = 20; + + // Distance from the crab to considered in the area + public static final int DISTANCE_THRESHOLD = 13; + + // Minutes at the boss required to count as a kill + // Also, used as a cooldown for mining so its not counted multiple times + public static final long KILL_THRESHOLD_MILLISECONDS = 5*60*1000; // 5 minutes + + // Location of each crab from its center + public static final WorldPoint EAST_CRAB = new WorldPoint(1353, 3112, 0); + public static final WorldPoint SOUTH_CRAB = new WorldPoint(1239,3043, 0); + public static final WorldPoint NORTH_CRAB = new WorldPoint(1273,3173, 0); + + // Crab Chat messages + public static final String GEMSTONE_CRAB_DEATH_MESSAGE = "The gemstone crab burrows away, leaving a piece of its shell behind."; + public static final String GEMSTONE_CRAB_MINE_SUCCESS_MESSAGE = "You swing your pick at the crab shell."; + public static final String GEMSTONE_CRAB_MINE_FAIL_MESSAGE = "Your understanding of the gemstone crab is not great enough to mine its shell."; + public static final String GEMSTONE_CRAB_GEM_MINE_MESSAGE = "You mine an uncut "; + public static final String GEMSTONE_CRAB_TOP16_MESSAGE = "You gained enough understanding of the crab to mine from its remains."; + public static final String GEMSTONE_CRAB_TOP3_MESSAGE = "The top three crab crushers were "; + + // Potions + public static final Set EXTENDED_ANTIFIRE_POTS = Set.of( + ItemID.EXTENDED_ANTIFIRE1, + ItemID.EXTENDED_ANTIFIRE2, + ItemID.EXTENDED_ANTIFIRE3, + ItemID.EXTENDED_ANTIFIRE4 + ); + public static final Set SUPER_EXTENDED_ANTIFIRE_POTS = Set.of( + ItemID.EXTENDED_SUPER_ANTIFIRE1, + ItemID.EXTENDED_SUPER_ANTIFIRE2, + ItemID.EXTENDED_SUPER_ANTIFIRE3, + ItemID.EXTENDED_SUPER_ANTIFIRE4 + ); + public static final Set PRAYER_POTS = Set.of( + ItemID.PRAYER_POTION1, + ItemID.PRAYER_POTION2, + ItemID.PRAYER_POTION3, + ItemID.PRAYER_POTION4 + ); + public static final Set SUPER_COMBAT_POTS = Set.of( + ItemID.SUPER_COMBAT_POTION1, + ItemID.SUPER_COMBAT_POTION2, + ItemID.SUPER_COMBAT_POTION3, + ItemID.SUPER_COMBAT_POTION4 + ); + public static final Set DIVINE_SUPER_COMBAT_POTS = Set.of( + ItemID.DIVINE_SUPER_COMBAT_POTION1, + ItemID.DIVINE_SUPER_COMBAT_POTION2, + ItemID.DIVINE_SUPER_COMBAT_POTION3, + ItemID.DIVINE_SUPER_COMBAT_POTION4 + ); + public static final Set DIGSITE_PENDANTS = Set.of( + ItemID.DIGSITE_PENDANT_1, + ItemID.DIGSITE_PENDANT_2, + ItemID.DIGSITE_PENDANT_3, + ItemID.DIGSITE_PENDANT_4, + ItemID.DIGSITE_PENDANT_5 + ); + +} diff --git a/src/main/java/com/example/GemCrabFighter/data/State.java b/src/main/java/com/example/GemCrabFighter/data/State.java new file mode 100644 index 00000000..85335044 --- /dev/null +++ b/src/main/java/com/example/GemCrabFighter/data/State.java @@ -0,0 +1,12 @@ +package com.example.GemCrabFighter.data; + +public enum State { + LOGOUT, + BANKING, + TRAVEL, + TIMEOUT, + CONSUME, + COMBAT, + MOVE_TO_COMBAT, + ANIMATING +} \ No newline at end of file diff --git a/src/main/java/com/example/GemCrabFighter/data/SubState.java b/src/main/java/com/example/GemCrabFighter/data/SubState.java new file mode 100644 index 00000000..211c1a6c --- /dev/null +++ b/src/main/java/com/example/GemCrabFighter/data/SubState.java @@ -0,0 +1,29 @@ +package com.example.GemCrabFighter.data; + +public enum SubState { + // Idle, + IDLE, + // Banking + FIND_BANK, + WITHDRAW, + DEPOSIT, + // Traveling + TELE_GE, + TELE_EDGE, + TELE_CIVITAS, + MOVE_DOWNSTAIRS, + OPEN_DOOR, + WALK_DOOR, + ENTER_LAIR, + DRINK_POOL, + // Consume, + ACTIVATE_PRAYER, + DEACTIVATE_PRAYER, + DRINK_POTIONS, + EAT_FOOD, + // Combat, + ATTACK_CRAB, + MINE_CRAB, + EQUIP_GEAR, + USE_SPECIAL, +} diff --git a/src/main/java/com/example/PiggyUtils/API/BankUtil.java b/src/main/java/com/example/PiggyUtils/API/BankUtil.java new file mode 100644 index 00000000..7611ce80 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/BankUtil.java @@ -0,0 +1,78 @@ +package com.example.PiggyUtils.API; + +import com.example.EthanApiPlugin.Collections.Bank; +import com.example.EthanApiPlugin.Collections.BankInventory; +import com.example.EthanApiPlugin.Collections.query.ItemQuery; +import com.example.EthanApiPlugin.EthanApiPlugin; +import com.example.Packets.MousePackets; +import com.example.Packets.WidgetPackets; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; + +import java.util.Collection; + +public class BankUtil { + + public static void depositAll(){ + Widget depositInventory = EthanApiPlugin.getClient().getWidget(WidgetInfo.BANK_DEPOSIT_INVENTORY); + if (depositInventory != null) { + MousePackets.queueClickPacket(); + WidgetPackets.queueWidgetAction(depositInventory, "Deposit inventory"); + } + } + + public static ItemQuery nameContainsNoCase(String name) { + return Bank.search().filter(widget -> widget.getName().toLowerCase().contains(name.toLowerCase())); + } + public static int getItemAmount(int itemId) { + return getItemAmount(itemId, false); + } + + public static int getItemAmount(int itemId, boolean stacked) { + return stacked ? + Bank.search().withId(itemId).first().map(Widget::getItemQuantity).orElse(0) : + Bank.search().withId(itemId).result().size(); + } + + public static int getItemAmount(String itemName) { + return nameContainsNoCase(itemName).result().size(); + } + + + public static boolean hasItem(int id) { + return hasItem(id, 1, false); + } + + public static boolean hasItem(int id, int amount) { + return getItemAmount(id, false) >= amount; + } + + public static boolean hasItem(int id, int amount, boolean stacked) { + return getItemAmount(id, stacked) >= amount; + } + + public static boolean hasAny(int ...ids) { + for (int id : ids) { + if (getItemAmount(id) > 0) { + return true; + } + } + return false; + } + + //good idea, credit to marcojacobsNL + public static boolean containsExcept(Collection ids) { + if (!Bank.isOpen()) { + return false; + } + Collection inventoryItems = BankInventory.search().result(); + + for (Widget item : inventoryItems) { + if (!ids.contains(item.getItemId())) { + return true; + } + } + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/PiggyUtils/API/EquipmentUtil.java b/src/main/java/com/example/PiggyUtils/API/EquipmentUtil.java new file mode 100644 index 00000000..7b8da6c4 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/EquipmentUtil.java @@ -0,0 +1,61 @@ +package com.example.PiggyUtils.API; + +import com.example.EthanApiPlugin.Collections.Equipment; +import com.example.EthanApiPlugin.Collections.EquipmentItemWidget; +import lombok.Getter; + +import java.util.Optional; +import java.util.function.Predicate; + +public class EquipmentUtil { + + public enum EquipmentSlot { + HEAD(0), CAPE(1), NECKLACE(2), MAIN_HAND(3), + TORSO(4), OFF_HAND(5), AMMO(13), LEGS(7), + HANDS(9), FEET(10), RING(12); + @Getter + private final int index; + + EquipmentSlot(int index) { + this.index = index; + } + } + + public static Optional getItemInSlot(EquipmentSlot slot) { + return Equipment.search().filter(item -> { + EquipmentItemWidget iw = (EquipmentItemWidget) item; + return iw.getEquipmentIndex() == slot.getIndex(); + }).first(); + } + + + public static boolean hasItem(String name) { + return Equipment.search().nameContainsNoCase(name).first().isPresent(); + } + + + public static boolean hasAnyItems(String... names) { + for (String name : names) { + if (hasItem(name)) { + return true; + } + } + + return false; + } + + @Deprecated + public static boolean hasItems(String... names) { + for (String name : names) { + if (!hasItem(name)) { + return false; + } + } + + return true; + } + + public static boolean hasItem(int id) { + return Equipment.search().withId(id).first().isPresent(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PiggyUtils/API/InventoryUtil.java b/src/main/java/com/example/PiggyUtils/API/InventoryUtil.java new file mode 100644 index 00000000..d6fee386 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/InventoryUtil.java @@ -0,0 +1,185 @@ +package com.example.PiggyUtils.API; + +import com.example.EthanApiPlugin.Collections.Inventory; +import com.example.EthanApiPlugin.Collections.query.ItemQuery; +import com.example.EthanApiPlugin.EthanApiPlugin; +import com.example.Packets.MousePackets; +import com.example.Packets.WidgetPackets; +import net.runelite.api.Varbits; +import net.runelite.api.widgets.Widget; + +import java.util.*; + +public class InventoryUtil { + public static boolean useItemNoCase(String name, String... actions) { + return nameContainsNoCase(name).first().flatMap(item -> + { + MousePackets.queueClickPacket(); + WidgetPackets.queueWidgetAction(item, actions); + return Optional.of(true); + }).orElse(false); + } + + public static ItemQuery nameContainsNoCase(String name) { + return Inventory.search().filter(widget -> widget.getName().toLowerCase().contains(name.toLowerCase())); + } + + public static Optional getById(int id) { + return Inventory.search().withId(id).first(); + } + + public static Optional getItemNameContains(String name, boolean caseSensitive) { + if (caseSensitive) { + return Inventory.search().filter(widget -> widget.getName().contains(name)).first(); + } else { + return Inventory.search().filter(widget -> widget.getName().toLowerCase().contains(name.toLowerCase())).first(); + } + } + + public static Optional getItemNameContains(String name) { + return getItemNameContains(name, true); + } + + public static Optional getItem(String name, boolean caseSensitive) { + if (caseSensitive) { + return Inventory.search().filter(widget -> widget.getName().equals(name)).first(); + } else { + return Inventory.search().filter(widget -> widget.getName().toLowerCase().equals(name.toLowerCase())).first(); + } + } + + public static Optional getItem(String name) { + return getItem(name, true); + } + + public static int getItemAmount(String name, boolean stacked) { + if (stacked) { + return nameContainsNoCase(name).first().isPresent() ? nameContainsNoCase(name).first().get().getItemQuantity() : 0; + } + + return nameContainsNoCase(name).result().size(); + } + + public static int getItemAmount(int id) { + return getItemAmount(id, false); + } + + public static int getItemAmount(int id, boolean stacked) { + if (stacked) { + return getById(id).isPresent() ? getById(id).get().getItemQuantity() : 0; + } + + return Inventory.search().withId(id).result().size(); + } + + public static boolean hasItem(String name) { + return getItemAmount(name, false) > 0; + } + + public static boolean hasItem(String name, boolean stacked) { + return getItemAmount(name, stacked) > 0; + } + + public static boolean hasItem(String name, int amount) { + return getItemAmount(name, false) >= amount; + } + + public static boolean hasItem(String name, int amount, boolean stacked) { + return getItemAmount(name, stacked) >= amount; + } + + public static boolean hasItems(String ...names) { + for (String name : names) { + if (!hasItem(name)) { + return false; + } + } + + return true; + } + + public static boolean hasAnyItems(String ...names) { + for (String name : names) { + if (hasItem(name)) { + return true; + } + } + + return false; + } + public static boolean hasAnyItems(Collection itemIds) { + for (Integer id : itemIds) { + if (hasItem(id)) { + return true; + } + } + return false; + } + + public static boolean hasItem(int id) { + return getItemAmount(id) > 0; + } + + public static List getItems() { + return Inventory.search().result(); + } + + public static int emptySlots() { + return 28 - Inventory.search().result().size(); + } + + //Credit to marcojacobsNL + public static boolean runePouchContains(int id) { + Set runePouchIds = new HashSet<>(); + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE1) != 0) { + runePouchIds.add(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE1)).getItemId()); + } + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE2) != 0) { + runePouchIds.add(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE2)).getItemId()); + } + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE3) != 0) { + runePouchIds.add(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE3)).getItemId()); + } + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE4) != 0) { + runePouchIds.add(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE4)).getItemId()); + } + for (int runePouchId : runePouchIds) { + if (runePouchId == id) { + return true; + } + } + return false; + } + + //Credit to marcojacobsNL + public static boolean runePouchContains(Collection ids) { + for (int runeId : ids) { + if (!runePouchContains(runeId)) { + return false; + } + } + return true; + } + + //Credit to marcojacobsNL + public static int runePouchQuanitity(int id) { + Map runePouchSlots = new HashMap<>(); + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE1) != 0) { + runePouchSlots.put(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE1)).getItemId(), EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_AMOUNT1)); + } + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE2) != 0) { + runePouchSlots.put(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE2)).getItemId(), EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_AMOUNT2)); + } + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE3) != 0) { + runePouchSlots.put(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE3)).getItemId(), EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_AMOUNT3)); + } + if (EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE4) != 0) { + runePouchSlots.put(Runes.getRune(EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_RUNE4)).getItemId(), EthanApiPlugin.getClient().getVarbitValue(Varbits.RUNE_POUCH_AMOUNT4)); + } + if (runePouchSlots.containsKey(id)) { + return runePouchSlots.get(id); + } + return 0; + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/PiggyUtils/API/MathUtil.java b/src/main/java/com/example/PiggyUtils/API/MathUtil.java new file mode 100644 index 00000000..9ce4d024 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/MathUtil.java @@ -0,0 +1,28 @@ +package com.example.PiggyUtils.API; + +import net.runelite.api.coords.WorldPoint; + +import java.util.concurrent.ThreadLocalRandom; + +public class MathUtil { + + public static int random(int min, int max){ + return ThreadLocalRandom.current().nextInt(min, max + 1); + } + + /** + * Returns a random tile +/- rX and +/- rY from the given tile + * @param wp + * @param rX + * @param rY + * @return + */ + public static WorldPoint randomizeTile(WorldPoint wp, int rX, int rY) { + return wp.dx(random(-rX, rX + 1)).dy(random(-rY, rY + 1)); + } + + public static WorldPoint randomizeTile2(WorldPoint wp, int rX, int rY) { + return wp.dx(random(rX, rX + 1)).dy(random(rY, rY + 1)); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/PiggyUtils/API/NpcUtil.java b/src/main/java/com/example/PiggyUtils/API/NpcUtil.java new file mode 100644 index 00000000..ff373ed0 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/NpcUtil.java @@ -0,0 +1,23 @@ +package com.example.PiggyUtils.API; + +import com.example.EthanApiPlugin.Collections.NPCs; +import com.example.EthanApiPlugin.Collections.query.NPCQuery; +import net.runelite.api.NPC; + +import java.util.Optional; + +public class NpcUtil { + + public static Optional getNpc(String name, boolean caseSensitive) { + if (caseSensitive) { + return NPCs.search().withName(name).nearestToPlayer(); + } else { + return nameContainsNoCase(name).nearestToPlayer(); + } + } + + public static NPCQuery nameContainsNoCase(String name) { + return NPCs.search().filter(npcs -> npcs.getName() != null && npcs.getName().toLowerCase().contains(name.toLowerCase())); + } + +} diff --git a/src/main/java/com/example/PiggyUtils/API/ObjectUtil.java b/src/main/java/com/example/PiggyUtils/API/ObjectUtil.java new file mode 100644 index 00000000..a33dfa6b --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/ObjectUtil.java @@ -0,0 +1,55 @@ +package com.example.PiggyUtils.API; + +import com.example.EthanApiPlugin.Collections.TileObjects; +import com.example.EthanApiPlugin.Collections.query.TileObjectQuery; +import net.runelite.api.ObjectComposition; +import net.runelite.api.TileObject; + +import java.util.Arrays; +import java.util.Optional; + +public class ObjectUtil { + + public static Optional getNearestBank() { + return TileObjects.search().filter(tileObject -> { + ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); + if (comp == null) + return false; + return comp.getName().toLowerCase().contains("bank") && Arrays.stream(comp.getActions()).anyMatch(action -> action.toLowerCase().contains("bank") || action.toLowerCase().contains("use") || action.contains("open")); + }).nearestToPlayer(); + } + + public static Optional getNearest(String name, boolean caseSensitive) { + if (caseSensitive) { + return TileObjects.search().withName(name).nearestToPlayer(); + } else { + return withNameNoCase(name).nearestToPlayer(); + } + } + + public static Optional getNearest(int id) { + return TileObjects.search().withId(id).nearestToPlayer(); + } + + public static Optional getNearestNameContains(String name) { + return nameContainsNoCase(name).nearestToPlayer(); + } + + public static TileObjectQuery withNameNoCase(String name) { + return TileObjects.search().filter(tileObject -> { + ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); + if (comp == null) + return false; + return comp.getName().toLowerCase().equals(name.toLowerCase()); + }); + } + + public static TileObjectQuery nameContainsNoCase(String name) { + return TileObjects.search().filter(tileObject -> { + ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); + if (comp == null) + return false; + return comp.getName().toLowerCase().contains(name.toLowerCase()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PiggyUtils/API/PlayerUtil.java b/src/main/java/com/example/PiggyUtils/API/PlayerUtil.java new file mode 100644 index 00000000..32497035 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/PlayerUtil.java @@ -0,0 +1,124 @@ +package com.example.PiggyUtils.API; + +import com.example.EthanApiPlugin.Collections.Inventory; +import com.example.EthanApiPlugin.Collections.NPCs; +import com.example.EthanApiPlugin.Collections.Players; +import com.example.EthanApiPlugin.Collections.query.NPCQuery; +import com.google.inject.Inject; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.api.VarPlayer; +import net.runelite.api.Varbits; +import net.runelite.api.coords.WorldArea; + +import java.util.List; + +@Slf4j +public class PlayerUtil { + @Inject + private Client client; + + /** + * 0- Auto retaliate on + * 1- Auto retaliate off + * @return + */ + public boolean isAutoRetaliating() { + return client.getVarpValue(172) == 0; + } + + public boolean inArea(WorldArea area) { + return area.contains(client.getLocalPlayer().getWorldLocation()); + } + + public boolean inRegion(int region) { + return client.getLocalPlayer().getWorldLocation().getRegionID() == region; + } + + /** + * Checks if the player is in any of the given regions + * + * @param regions + * @return + */ + public boolean inRegion(int... regions) { + for (int region : regions) { + if (inRegion(region)) { + return true; + } + } + return false; + } + + public boolean hasItem(String name) { + return Inventory.getItemAmount(name) > 0; + } + + public boolean hasItem(int id) { + return Inventory.getItemAmount(id) > 0; + } + + /** + * Run energy the way we read it + * + * @return + */ + public int runEnergy() { + return client.getEnergy() / 100; + } + + /** + * Spec energy the way we read it + * @return + */ + public int specEnergy() { + return client.getVarpValue(VarPlayer.SPECIAL_ATTACK_PERCENT) / 10; + } + + public int hp() { + return client.getBoostedSkillLevel(Skill.HITPOINTS); + } + + + public boolean isStaminaActive() { + return client.getVarbitValue(Varbits.RUN_SLOWED_DEPLETION_ACTIVE) == 1; + } + + public boolean isRunning() { + return client.getVarpValue(173) == 0; + } + + public boolean inMulti() { + return client.getVarbitValue(Varbits.MULTICOMBAT_AREA) == 1; + } + + public boolean isInteracting() { + return client.getLocalPlayer().isInteracting(); + } + + public boolean isBeingInteracted() { + return NPCs.search().interactingWithLocal().first().isPresent(); + } + + public boolean isBeingInteracted(String name) { + return NPCs.search().filter(npc -> npc.getName() != null && npc.getName().equalsIgnoreCase(name)).interactingWithLocal().first().isPresent(); + } + + public NPCQuery getBeingInteracted(String name) { + return NPCs.search().filter(npc -> npc.getName() != null && npc.getName().equalsIgnoreCase(name)).interactingWithLocal(); + } + public NPCQuery getBeingInteracted(List names) { + return NPCs.search().filter(npc -> npc.getName() != null && names.contains(npc.getName())).interactingWithLocal(); + } + + /** + * Slayer task count + * + * @return + */ + public int getTaskCount() { + return client.getVarpValue(VarPlayer.SLAYER_TASK_SIZE); + } + +} diff --git a/src/main/java/com/example/PiggyUtils/API/PrayerUtil.java b/src/main/java/com/example/PiggyUtils/API/PrayerUtil.java new file mode 100644 index 00000000..038236b6 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/PrayerUtil.java @@ -0,0 +1,66 @@ +package com.example.PiggyUtils.API; + +import com.example.EthanApiPlugin.EthanApiPlugin; +import com.example.PacketUtils.WidgetInfoExtended; +import com.example.Packets.MousePackets; +import com.example.Packets.WidgetPackets; +import net.runelite.api.Prayer; + +import java.util.HashMap; + +public class PrayerUtil { + public static final HashMap prayerMap = new HashMap<>(); + static{ + prayerMap.put(Prayer.AUGURY,WidgetInfoExtended.PRAYER_AUGURY); // + prayerMap.put(Prayer.BURST_OF_STRENGTH,WidgetInfoExtended.PRAYER_BURST_OF_STRENGTH); // + prayerMap.put(Prayer.CHIVALRY,WidgetInfoExtended.PRAYER_CHIVALRY); // + prayerMap.put(Prayer.CLARITY_OF_THOUGHT,WidgetInfoExtended.PRAYER_CLARITY_OF_THOUGHT); // + prayerMap.put(Prayer.EAGLE_EYE,WidgetInfoExtended.PRAYER_EAGLE_EYE); // + prayerMap.put(Prayer.HAWK_EYE,WidgetInfoExtended.PRAYER_HAWK_EYE); // + prayerMap.put(Prayer.IMPROVED_REFLEXES,WidgetInfoExtended.PRAYER_IMPROVED_REFLEXES); // + prayerMap.put(Prayer.INCREDIBLE_REFLEXES,WidgetInfoExtended.PRAYER_INCREDIBLE_REFLEXES); // + prayerMap.put(Prayer.MYSTIC_MIGHT,WidgetInfoExtended.PRAYER_MYSTIC_MIGHT); // + prayerMap.put(Prayer.PIETY,WidgetInfoExtended.PRAYER_PIETY); // + prayerMap.put(Prayer.PRESERVE,WidgetInfoExtended.PRAYER_PRESERVE); // + prayerMap.put(Prayer.PROTECT_FROM_MAGIC,WidgetInfoExtended.PRAYER_PROTECT_FROM_MAGIC); // + prayerMap.put(Prayer.PROTECT_FROM_MELEE,WidgetInfoExtended.PRAYER_PROTECT_FROM_MELEE); // + prayerMap.put(Prayer.PROTECT_FROM_MISSILES,WidgetInfoExtended.PRAYER_PROTECT_FROM_MISSILES); // + prayerMap.put(Prayer.RETRIBUTION,WidgetInfoExtended.PRAYER_RETRIBUTION); // + prayerMap.put(Prayer.RIGOUR,WidgetInfoExtended.PRAYER_RIGOUR); // + prayerMap.put(Prayer.ROCK_SKIN,WidgetInfoExtended.PRAYER_ROCK_SKIN); // + prayerMap.put(Prayer.SHARP_EYE,WidgetInfoExtended.PRAYER_SHARP_EYE); // + prayerMap.put(Prayer.SMITE,WidgetInfoExtended.PRAYER_SMITE); // + prayerMap.put(Prayer.STEEL_SKIN,WidgetInfoExtended.PRAYER_STEEL_SKIN); // + prayerMap.put(Prayer.THICK_SKIN,WidgetInfoExtended.PRAYER_THICK_SKIN); // + prayerMap.put(Prayer.ULTIMATE_STRENGTH,WidgetInfoExtended.PRAYER_ULTIMATE_STRENGTH); // + prayerMap.put(Prayer.REDEMPTION,WidgetInfoExtended.PRAYER_REDEMPTION); // + prayerMap.put(Prayer.RAPID_RESTORE,WidgetInfoExtended.PRAYER_RAPID_RESTORE); // + prayerMap.put(Prayer.RAPID_HEAL,WidgetInfoExtended.PRAYER_RAPID_HEAL); // + prayerMap.put(Prayer.PROTECT_ITEM,WidgetInfoExtended.PRAYER_PROTECT_ITEM); // + prayerMap.put(Prayer.MYSTIC_LORE,WidgetInfoExtended.PRAYER_MYSTIC_LORE); // + prayerMap.put(Prayer.SUPERHUMAN_STRENGTH,WidgetInfoExtended.PRAYER_SUPERHUMAN_STRENGTH); // + prayerMap.put(Prayer.MYSTIC_WILL,WidgetInfoExtended.PRAYER_MYSTIC_WILL); // + } + + public static int getPrayerWidgetId(Prayer prayer) { + return prayerMap.getOrDefault(prayer, WidgetInfoExtended.PRAYER_THICK_SKIN).getPackedId(); + } + + public static boolean isPrayerActive(Prayer prayer) { + return EthanApiPlugin.getClient().isPrayerActive(prayer); + } + + public static void togglePrayer(Prayer prayer) { + if (prayer == null) { + return; + } + MousePackets.queueClickPacket(); + WidgetPackets.queueWidgetActionPacket(1, getPrayerWidgetId(prayer), -1, -1); + } + + public static void toggleMultiplePrayers(Prayer ...prayers) { + for (Prayer prayer : prayers) { + togglePrayer(prayer); + } + } +} diff --git a/src/main/java/com/example/PiggyUtils/API/Runes.java b/src/main/java/com/example/PiggyUtils/API/Runes.java new file mode 100644 index 00000000..6d6c73cf --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/Runes.java @@ -0,0 +1,73 @@ +package com.example.PiggyUtils.API; + +/** + * Taken from marcojacobsNL + * https://github.com/marcojacobsNL/runelite-plugins/blob/master/src/main/java/com/koffee/KoffeeUtils/Runes.java + */ + +import com.google.common.collect.ImmutableMap; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.awt.image.BufferedImage; +import java.util.Map; + +import static net.runelite.api.ItemID.*; + +public enum Runes { + AIR(1, AIR_RUNE), + WATER(2, WATER_RUNE), + EARTH(3, EARTH_RUNE), + FIRE(4, FIRE_RUNE), + MIND(5, MIND_RUNE), + CHAOS(6, CHAOS_RUNE), + DEATH(7, DEATH_RUNE), + BLOOD(8, BLOOD_RUNE), + COSMIC(9, COSMIC_RUNE), + NATURE(10, NATURE_RUNE), + LAW(11, LAW_RUNE), + BODY(12, BODY_RUNE), + SOUL(13, SOUL_RUNE), + ASTRAL(14, ASTRAL_RUNE), + MIST(15, MIST_RUNE), + MUD(16, MUD_RUNE), + DUST(17, DUST_RUNE), + LAVA(18, LAVA_RUNE), + STEAM(19, STEAM_RUNE), + SMOKE(20, SMOKE_RUNE), + WRATH(21, WRATH_RUNE); + + private static final Map runes; + + static { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (Runes rune : values()) { + builder.put(rune.getId(), rune); + } + runes = builder.build(); + } + + @Getter(AccessLevel.PACKAGE) + private final int id; + @Getter(AccessLevel.PACKAGE) + private final int itemId; + @Getter(AccessLevel.PACKAGE) + @Setter(AccessLevel.PACKAGE) + private BufferedImage image; + + Runes(final int id, final int itemId) { + this.id = id; + this.itemId = itemId; + } + + public static Runes getRune(int varbit) { + return runes.get(varbit); + } + + public String getName() { + String name = this.name(); + name = name.substring(0, 1) + name.substring(1).toLowerCase(); + return name; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PiggyUtils/API/SpellUtil.java b/src/main/java/com/example/PiggyUtils/API/SpellUtil.java new file mode 100644 index 00000000..020ef869 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/API/SpellUtil.java @@ -0,0 +1,20 @@ +package com.example.PiggyUtils.API; + +import com.example.PacketUtils.WidgetInfoExtended; +import net.runelite.api.Client; +import net.runelite.api.widgets.Widget; + +public class SpellUtil { + public static WidgetInfoExtended parseStringForWidgetInfoExtended(String input) { + for (WidgetInfoExtended value : WidgetInfoExtended.values()) { + if (value.name().equalsIgnoreCase("SPELL_" + input.replace(" ", "_"))) { + return value; + } + } + return null; + } + + public static Widget getSpellWidget(Client client, String input) { + return client.getWidget(parseStringForWidgetInfoExtended(input).getPackedId()); + } +} diff --git a/src/main/java/com/example/PiggyUtils/PiggyUtilsPlugin.java b/src/main/java/com/example/PiggyUtils/PiggyUtilsPlugin.java new file mode 100644 index 00000000..7d49d641 --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/PiggyUtilsPlugin.java @@ -0,0 +1,16 @@ +package com.example.PiggyUtils; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; + +@PluginDescriptor(name = "[PP] PiggyUtils", + description = "Utility Plugin for PiggyPlugins", + tags = {"piggy","ethan"}) +@Slf4j +public class PiggyUtilsPlugin extends Plugin { + @Override + protected void startUp() throws Exception { + log.info("[PiggyUtils] Piggy Utils started"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/PiggyUtils/RLApi/GraphicIDExtended.java b/src/main/java/com/example/PiggyUtils/RLApi/GraphicIDExtended.java new file mode 100644 index 00000000..c9a21b9b --- /dev/null +++ b/src/main/java/com/example/PiggyUtils/RLApi/GraphicIDExtended.java @@ -0,0 +1,96 @@ +package com.example.PiggyUtils.RLApi; + +public final class GraphicIDExtended { + public static final int POISON_SPLAT = 184; + + public static final int OLM_BURN = 1351; + public static final int OLM_LIGHTNING = 1356; + public static final int OLM_TELEPORT = 1359; + public static final int OLM_HEAL = 1363; + public static final int OLM_CRYSTAL = 1447; + public static final int XERIC_TELEPORT = 1612; + + public static final int LIZARDMAN_SHAMAN_AOE = 1293; + public static final int CRAZY_ARCHAEOLOGIST_AOE = 1260; + public static final int ICE_DEMON_RANGED_AOE = 1324; + public static final int ICE_DEMON_ICE_BARRAGE_AOE = 366; + public static final int VASA_AWAKEN_AOE = 1327; + public static final int VASA_RANGED_AOE = 1329; + public static final int TEKTON_METEOR_AOE = 660; + + public static final int OLM_FALLING_CRYSTAL = 1357; + public static final int OLM_BURNING = 1349; + public static final int OLM_FALLING_CRYSTAL_TRAIL = 1352; + public static final int OLM_ACID_TRAIL = 1354; + public static final int OLM_FIRE_LINE = 1347; + public static final int OLM_MAGE_ATTACK = 1339; + public static final int OLM_RANGE_ATTACK = 1340; + + public static final int VORKATH_BOMB_AOE = 1481; + public static final int VORKATH_POISON_POOL_AOE = 1483; + public static final int VORKATH_TICK_FIRE_AOE = 1482; + public static final int VORKATH_SPAWN_AOE = 1484; + + public static final int ADDY_DRAG_POISON = 1486; + + public static final int GALVEK_MINE = 1495; + public static final int GALVEK_BOMB = 1491; + + public static final int DAWN_FREEZE = 1445; + public static final int DUSK_CEILING = 1435; + + public static final int VETION_LIGHTNING = 280; + + public static final int CHAOS_FANATIC_AOE = 551; + + public static final int CORPOREAL_BEAST_AOE = 315; + public static final int CORPOREAL_BEAST_DARK_CORE_AOE = 319; + + public static final int WINTERTODT_SNOW_FALL_AOE = 1310; + + public static final int DEMONIC_GORILLA_RANGED = 1302; + public static final int DEMONIC_GORILLA_MAGIC = 1304; + public static final int DEMONIC_GORILLA_BOULDER = 856; + + public static final int XARPUS_ACID = 1555; + public static final int VERZIK_PURPLE_SPAWN = 1586; + public static final int CERB_FIRE = 1247; + + public static final int DERWEN_HEALING_BALL = 1512; + public static final int JUSTICIAR_LEASH = 1515; + public static final int MAGE_ARENA_BOSS_FREEZE = 368; + + /** + * missing: superior dark beast + */ + public static final int MARBLE_GARGOYLE_AOE = 1453; + /** + * non AOE, regular projectiles + */ + public static final int VORKATH_DRAGONBREATH = 393; + public static final int VORKATH_RANGED = 1477; + public static final int VORKATH_MAGIC = 1479; + public static final int VORKATH_PRAYER_DISABLE = 1471; + public static final int VORKATH_VENOM = 1470; + public static final int VORKATH_ICE = 395; + + public static final int SOTETSEG_BOMB = 1604; + public static final int SOTETSEG_RANGE = 1607; + public static final int SOTETSEG_MAGE = 1606; + + public static final int HYDRA_MAGIC = 1662; + public static final int HYDRA_RANGED = 1663; + public static final int HYDRA_POISON = 1644; + public static final int HYDRA_LIGHTNING = 1664; + public static final int HYDRA_LIGHTNING_2 = 1665; + public static final int DRAKE_BREATH = 1637; + + public static final int HUNLLEF_MAGE_ATTACK = 1707; + public static final int HUNLLEF_CORRUPTED_MAGE_ATTACK = 1708; + public static final int HUNLLEF_RANGE_ATTACK = 1711; + public static final int HUNLLEF_CORRUPTED_RANGE_ATTACK = 1712; + public static final int HUNLLEF_PRAYER_ATTACK = 1713; + public static final int HUNLLEF_CORRUPTED_PRAYER_ATTACK = 1714; + + public static final int PORAZDIR_ENERGY_BALL = 1514; +} \ No newline at end of file diff --git a/src/main/java/com/example/PowerSkiller/PowerSkillerConfig.java b/src/main/java/com/example/PowerSkiller/PowerSkillerConfig.java index e5a4b463..1d114118 100644 --- a/src/main/java/com/example/PowerSkiller/PowerSkillerConfig.java +++ b/src/main/java/com/example/PowerSkiller/PowerSkillerConfig.java @@ -91,6 +91,17 @@ default String itemsToKeep() { } + @ConfigItem( + name = "Tick Delay", + keyName = "tickDelay", + description = "Slow down plugin", + position = 1, + section = tickDelaySection + ) + default int tickDelay() { + return 0; + } + @ConfigItem( name = "Tick Delay Min", keyName = "tickDelayMin", diff --git a/src/main/java/com/example/PowerSkiller/PowerSkillerPlugin.java b/src/main/java/com/example/PowerSkiller/PowerSkillerPlugin.java index 48554aa4..35e77117 100644 --- a/src/main/java/com/example/PowerSkiller/PowerSkillerPlugin.java +++ b/src/main/java/com/example/PowerSkiller/PowerSkillerPlugin.java @@ -3,9 +3,11 @@ import com.example.EthanApiPlugin.Collections.*; import com.example.EthanApiPlugin.EthanApiPlugin; import com.example.InteractionApi.BankInventoryInteraction; +import com.example.EthanApiPlugin.Collections.query.TileObjectQuery; import com.example.InteractionApi.InventoryInteraction; import com.example.InteractionApi.NPCInteraction; import com.example.InteractionApi.TileObjectInteraction; +import com.example.EthanApiPlugin.Collections.query.TileObjectQuery; import com.google.inject.Provides; import net.runelite.api.*; import net.runelite.api.GameObject; @@ -19,12 +21,14 @@ import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.util.HotkeyListener; +import ChinBreakHandler.ChinBreakHandler; import lombok.extern.slf4j.Slf4j; import com.google.inject.Inject; import net.runelite.client.util.Text; import org.apache.commons.lang3.RandomUtils; +import java.time.Instant; import java.util.*; @Slf4j @@ -44,6 +48,8 @@ public class PowerSkillerPlugin extends Plugin { private OverlayManager overlayManager; @Inject private PowerSkillerOverlay overlay; + @Inject + private ChinBreakHandler chinBreakHandler; State state; boolean started; @@ -53,17 +59,18 @@ public class PowerSkillerPlugin extends Plugin { @Override protected void startUp() throws Exception { - log.info("startUp Powerskiller"); + chinBreakHandler.registerPlugin(this, true); bankPin = false; keyManager.registerKeyListener(toggle); this.overlayManager.add(overlay); - log.info("Ending Startup Powerskiller"); } + @Override protected void shutDown() throws Exception { bankPin = false; keyManager.unregisterKeyListener(toggle); + chinBreakHandler.unregisterPlugin(this); this.overlayManager.remove(overlay); } @@ -85,13 +92,40 @@ private void onGameTick(GameTick event) { // We do an early return if the user isn't logged in return; } + + + // DETAILED DEBUGGING + boolean breakActive = chinBreakHandler.isBreakActive(this); + Map activeBreaks = chinBreakHandler.getActiveBreaks(); + Map plannedBreaks = chinBreakHandler.getPlannedBreaks(); + + if (tickCounter % 50 == 0) { // Log every 50 ticks + log.info("=== Break Handler Status ==="); + log.info("isBreakActive: {}", breakActive); + log.info("Active breaks size: {}", activeBreaks.size()); + log.info("Planned breaks size: {}", plannedBreaks.size()); + log.info("This plugin in active: {}", activeBreaks.containsKey(this)); + log.info("This plugin in planned: {}", plannedBreaks.containsKey(this)); + + if (!plannedBreaks.isEmpty()) { + for (Map.Entry entry : plannedBreaks.entrySet()) { + log.info("Planned: {} at {}", entry.getKey().getName(), entry.getValue()); + } + } + } + + if (breakActive) { + log.info("Break is active, pausing plugin"); + return; + } + state = getNextState(); if (tickCounter % debugEveryXTicks == 0) { - log.info("[PS] state={}, invFull={}, anim={}, moving={}", + /*log.info("[PS] state={}, invFull={}, anim={}, moving={}", state, Inventory.full(), client.getLocalPlayer().getAnimation(), - EthanApiPlugin.isMoving()); + EthanApiPlugin.isMoving()); */ } handleState(); @@ -100,56 +134,82 @@ private void onGameTick(GameTick event) { private void handleState() { switch (state) { case BANK: + // Handle bank PIN if (Widgets.search().withId(13959169).first().isPresent()) { bankPin = true; return; } - if (Widgets.search().withId(786445).first().isEmpty()) { - TileObjects.search().withAction("Bank").nearestToPlayer().ifPresent(tileObject -> { - TileObjectInteraction.interact(tileObject, "Bank"); - return; - }); - /* Outdated - check to fix later. - NPCs.search().withAction("Bank").nearestToPlayer().ifPresent(npc -> { - if (EthanApiPlugin.pathToGoalSet(npc.getWorldLocation(), new HashSet<>(), new HashSet<>(), new HashSet<>(), null) != null) { - NPCInteraction.interact(npc, "Bank"); + + // If bank is already open, deposit items + if (Widgets.search().withId(786445).first().isPresent()) { + List items = BankInventory.search().result(); + for (Widget item : items) { + if (!isTool(item.getName().toLowerCase()) && + !shouldKeep(item.getName().toLowerCase())) { + BankInventoryInteraction.useItem(item, "Deposit-All"); + return; } - return; - }); */ - TileObjects.search().withName("Bank chest").nearestToPlayer().ifPresent(tileObject -> { - TileObjectInteraction.interact(tileObject, "Use"); - return; - }); - if (TileObjects.search().withAction("Bank").nearestToPlayer().isEmpty() && NPCs.search().withAction("Bank").nearestToPlayer().isEmpty()) { - EthanApiPlugin.sendClientMessage("Bank is not found, move to an area with a bank."); } - + // All items deposited, change state + state = State.FIND_OBJECT; // or whatever your next state is return; + } + // Bank not open - try to open it + // Try bank objects first + Optional bankObject = TileObjects.search() + .withAction("Bank") + .nearestToPlayer(); + + if (bankObject.isPresent()) { + TileObjectInteraction.interact(bankObject.get(), "Bank"); + timeout = 5; // Wait for bank to open + return; } - List items = BankInventory.search().result(); - for (Widget item : items) { - if (!isTool(item.getName().toLowerCase()) && !shouldKeep(item.getName().toLowerCase())) { - BankInventoryInteraction.useItem(item, "Deposit-All"); - return; - } + + // Try bank chests + Optional bankChest = TileObjects.search() + .withName("Bank chest") + .nearestToPlayer(); + + if (bankChest.isPresent()) { + TileObjectInteraction.interact(bankChest.get(), "Use"); + timeout = 5; + return; } + + /* Uncomment when fixed + Optional banker = NPCs.search() + .withAction("Bank") + .nearestToPlayer(); + + if (banker.isPresent()) { + NPCInteraction.interact(banker.get(), "Bank"); + timeout = 5; + return; + } + */ + + // Only show error if nothing was found + EthanApiPlugin.sendClientMessage("Bank is not found, move to an area with a bank."); break; + case TIMEOUT: timeout--; + if (timeout <= 0) { + state = State.BANK; // or appropriate next state + } break; + case FIND_OBJECT: if (config.searchNpc()) { findNpc(); } else { - - GameObject gameObj = findObject(config.objectToInteract()); - if(gameObj != null) { - log.info("GameObject found: " + gameObj.getId()); - } + findObject(); } setTimeout(); break; + case DROP_ITEMS: dropItems(); break; @@ -158,6 +218,7 @@ private void handleState() { private State getNextState() { // self-explanatory, we just return a State if the conditions are met. + if (EthanApiPlugin.isMoving() || client.getLocalPlayer().getAnimation() != -1) { // this is to prevent clicks while animating/moving. return State.ANIMATING; @@ -168,10 +229,12 @@ private State getNextState() { return State.MISSING_TOOLS; } if (timeout > 0) { + // If the timeout is above 0, we want to decrement it and return the timeout state. return State.TIMEOUT; } if (shouldBank() && Inventory.full() || (Bank.isOpen() && !isInventoryReset())) { + // If the user should be banking and the inventory is full, or if the bank is open and the inventory isn't reset, we want to bank. if (shouldBank() && !isInventoryReset()) { return State.BANK; } @@ -179,11 +242,9 @@ private State getNextState() { if ((isDroppingItems() && !isInventoryReset()) || !shouldBank() && Inventory.full()) { // if the user should be dropping items, we'll check if they're done - // should sit at this state til it's finished. return State.DROP_ITEMS; } - // default it'll look for an object. return State.FIND_OBJECT; } @@ -194,27 +255,20 @@ private State getNextState() { // Handle multiple matches, i.e. // 3. if not, return null - private GameObject findObject(String objectName) { - final Scene scene = client.getScene(); - - for (Tile[][] plane : scene.getTiles()) { - for (Tile[] row : plane) { - for (Tile tile : row) { - if (tile != null) { - for (GameObject obj : tile.getGameObjects()) { - if (obj != null) { - ObjectComposition def = client.getObjectDefinition(obj.getId()); - if (def.getName().equalsIgnoreCase(objectName)) { - log.info("object match! Name of object: "+ def.getName()); - return obj; - } - } - } - } - } - } + private void findObject() { + String objectName = config.objectToInteract(); + if (config.useForestryTreeNotClosest() && config.expectedAction().equalsIgnoreCase("chop")) { + TileObjects.search().withName(objectName).nearestToPoint(getObjectWMostPlayers()).ifPresent(tileObject -> { + ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); + TileObjectInteraction.interact(tileObject, comp.getActions()[0]); + }); + } else { + TileObjects.search().withName(objectName).nearestToPlayer().ifPresent(tileObject -> { + ObjectComposition comp = TileObjectQuery.getObjectComposition(tileObject); + TileObjectInteraction.interact(tileObject, comp.getActions()[0]); // find the object we're looking for. this specific example will only work if the first Action the object has is the one that interacts with it. + // don't *always* do this, you can manually type the possible actions. eg. "Mine", "Chop", "Cook", "Climb". + }); } - return null; // Not found } /** @@ -250,14 +304,8 @@ public WorldPoint getObjectWMostPlayers() { private void findNpc() { String npcName = config.objectToInteract(); - log.info("NPCName:"+ npcName); - log.info("NPCs nearesttoPlayer: "+ NPCs.search().withName(npcName).nearestToPlayer()); - log.info("NPCs search: "+ NPCs.search().withName(npcName)); NPCs.search().withName(npcName).nearestToPlayer().ifPresent(npc -> { - log.info("NPC: "+ npc); NPCComposition comp = client.getNpcDefinition(npc.getId()); - log.info("comp NPC: "+ comp); - log.info("comp.getActions(): "+ comp.getActions()[0]); if (Arrays.stream(comp.getActions()).anyMatch(action -> action.equalsIgnoreCase(config.expectedAction()))) { NPCInteraction.interact(npc, config.expectedAction()); // For fishing spots ? } else { @@ -268,7 +316,6 @@ private void findNpc() { } private void dropItems() { - System.out.println("Inside dropItems"); List itemsToDrop = Inventory.search() .filter(item -> !shouldKeep(item.getName()) && !isTool(item.getName())).result(); // filter the inventory to only get the items we want to drop @@ -337,9 +384,21 @@ public void hotkeyPressed() { }; private boolean shouldBank() { - return config.shouldBank() && - (NPCs.search().withAction("Bank").first().isPresent() || TileObjects.search().withAction("Bank").first().isPresent() - || TileObjects.search().withAction("Collect").first().isPresent() && !bankPin); + return true; + /* + boolean configShouldBank = config.shouldBank(); + boolean tileBankPresent = TileObjects.search().withAction("Bank").nearestToPlayer().isPresent(); + boolean tileBankChestPresent = TileObjects.search().withName("Bank chest").nearestToPlayer().isPresent(); + boolean npcBankPresent = NPCs.search().withAction("Bank").nearestToPlayer().isPresent(); + boolean result = configShouldBank && tileBankPresent && npcBankPresent && !bankPin; + /*log.info("Tileobject search first(): " + TileObjects.search().withAction("Bank").first()); + log.info("Tileobject search neareastToPlayer(): " + TileObjects.search().withAction("Bank").nearestToPlayer()); + log.info("Should bank? {} (config={}, tileBankPresent={}, npcBankPresent={}, bankPin={}, tileBankChestPresent={})", + result, configShouldBank, tileBankPresent, npcBankPresent, bankPin, tileBankChestPresent); */ /* + log.info("pathtogoal testing: "+ EthanApiPlugin.pathToGoalFromPlayerNoCustomTiles(new WorldPoint(3092,3246,0))); + EthanApiPlugin.pathToGoalFromPlayerNoCustomTiles(new WorldPoint(3092,3246,0)); + + return result; */ } public void toggle() { @@ -347,5 +406,13 @@ public void toggle() { return; } started = !started; + + if (started) { + chinBreakHandler.startPlugin(this); + + // Force an immediate break for testing + chinBreakHandler.planBreak(this, Instant.now().plusSeconds(5)); + log.info("Forced break planned in 5 seconds"); + } } } \ No newline at end of file diff --git a/src/main/resources/chin_special.png b/src/main/resources/chin_special.png new file mode 100644 index 0000000000000000000000000000000000000000..772a3cf652d5e0c18fc939dcebfd2e0878045d77 GIT binary patch literal 1816 zcmV+z2j}>SP)irRC{!ksX@&Uz=Mr+cd`-GcX3t1Vn?XFxl*>SuuL6ZU^{V*ZSn1QVBVknS>kSMaveW`#RSS@9UY}yTb`?-U5|nrLbAA z2ZL4v1t?}?b~a?Er{7X4l`o6eE=!yme|>Od{{Yx5bx>DP0X602D54BD8w^lPA~tRW zd1@*I2L}ESD3OdIs{H-^0}!iXVq#X}7SB{4-Zrb{UQ2yFG}YFCwYm!GD=VSeY=-jE zQbebh=*UQj3=fBhurLS@34zd{pg%)`f_@2-N`I0{B;TN-qw9Tr&0>Nl zMoUY}D$Z)fr?6rn~IA;tx~~;goFi%kBwa*W=;s82ta2bz|RkY0s^iC z`S|#VxfTt(-TsozYW>;G+u+ml??H{Z40ErA;=)2mOHN)uPEN$d#4wNm0g-c#M23e! zBHlyagQe2%aC>ht-=fh_T^-+AUq5?xas=G%O`tC*fPBomr+~!xcm_d4OjOhY5kzb@ z8nJHlVAK^-;odc9G?8Mi#|GwoyNheN?c%NXsrZOo?0_JI1PE#Hgg!qXjObi(p%$-N zKmk);OLc)ltJSV4GZ_BJ$J`TnYElv;Z`kk;5)+vS5 zM5R#t6&)QNDCT)==rx)%6?pt)DJk;~cp?Qw2!)uPnE?rSC35;*k99+*rU=tV{e${Q zE;cYcMPGU?<(TUA`n7oW@0H;~8ImmqazrS`wB#gax4Fa!Ep>Ga%?N9CHP9X=Ort_B zhb%;9YHG2VZ$S(V4ZVO1u2`xnf%*Y0#zl~hP6!|YLQPN|(3OrWH7vfV2^?*0h*k!^ zsS(OiSf)%y9Tc);vfDIc#JuxiDk_rJmX|+hs;vd;34~f~L<-zMEI_D*Bm^B*TnBa7 zxH{W;=yf_^i=zWSwnUw|E85zt4bVDWPoeyk2Ur`5avowIcxfwcITG*A`$iWRr zh>!-)n#jWcXkBAXHSE~xgn`aZ6ySg^dpkHe8}v9Vj|89#=!8wo%Yx>7b6qW9GlF)s z(rL5dVh$X5C0n`1Mm7sFL@6O;VwH$nN0aZsYlD5z=W;OtF5JV-wL)HQ4od?* z$cUJm1qxV6mJ?Y|M+dW!HgNMigCK_T+=wYEFDYRonp`1R(Eej%`{44&M_^?CF4)%9 z1-|keq6SK%Zaj?S|`DK7g6&lW_jS!?3Hr4+gut@#&k{IH%^c zqpuZ*Trr6_&>phhH615~%Lui^cZwZBoP@KamIws_WF>)$irna$bAxir+?W$F*yD!l zmnY$K#LV;w7(zkVOApW0A%Uw%J{>EC`apmFQ5q^Mqqv5K zGzZ6}^*9_iwmO}xm^{VMm3|{Zlo}=i9oW|GhR-fdz@>9XVCLcp*fZ3F-7udC$ji-T zM{#FAhv_ndsYnMC(E&`ru_zqfYuEZOEC?^wlwL#vBf9asAY?Jne{|;i+)fznX@&lR zES8GX6NlJ1FT|NZGX;wek9Q4Ns;a=0n#{-vTSEOO#oP;`o8x@DcrA)!)fvzX^U zqswl;K_WcSZMO?}aDHm^)VcTHbe(%OSr!fDn z_=sCxOPP-1=(5>#L?#w7CuF<${>kCt)!^|m66Wh0vn9m;R;9#mkLP_)6W-Vpex$zO z3G5LMIF4H_zWE8U^fx4NbaLDTH6;%L51l-pA-?aq1vV=kH6uYpr=7p%^(;^Hv_URZQ2d)888$1ki|m)58C-O{@?uvY5bGuX1m zil7_o$lA6l>%h8A?;fr72$_Z#FV?Ff2yl03?dDBp-}+D_>vT3$l(Unbaq^}~Sq}663I^aEJ>^%iJk?);Iuq!546mIaQRA8ClQ8^NAvc)2zWFi?d)1 za7=K977#<3H91jN^RSMS`}9Avp@=DyciT+xMf%hJ2rh}?x28&q=jCF8zU7lFxmKJq zH=qH{qz~;NNQ*0`Z^GTC=0ZJogaFDU*PtE~q_GE10}27zoa-+0`t)Dmbv?dZddB-jKpjV1{>+NmCJHjk z@?}l$i?qcI8(b?UeK@9c?abSyYt%3Dwmm$1bdI#Csj2G4v#OFAnVFg2)j7)MWF_pZ z+1&h*oum22UI)VoGP|laHSXHAYf4*N+um8Tq8NbUAs-JeU}L!Z-P>l(IbP0l+XkK>KTP1 literal 0 HcmV?d00001 diff --git a/src/test/java/com/example/ExamplePluginTest.java b/src/test/java/com/example/ExamplePluginTest.java index fbfaa0c3..1f71c2ea 100644 --- a/src/test/java/com/example/ExamplePluginTest.java +++ b/src/test/java/com/example/ExamplePluginTest.java @@ -3,15 +3,15 @@ import net.runelite.client.RuneLite; import net.runelite.client.externalplugins.ExternalPluginManager; import com.example.PowerSkiller.PowerSkillerPlugin; -import com.example.PowerSkiller.PSDummy; import com.example.EthanApiPlugin.EthanApiPlugin; import com.example.PacketUtils.PacketUtilsPlugin; +import ChinBreakHandler.ChinBreakHandlerPlugin; public class ExamplePluginTest { public static void main(String[] args) throws Exception { - ExternalPluginManager.loadBuiltin(PacketUtilsPlugin.class, EthanApiPlugin.class, PowerSkillerPlugin.class); + ExternalPluginManager.loadBuiltin(PacketUtilsPlugin.class, EthanApiPlugin.class, PowerSkillerPlugin.class, ChinBreakHandlerPlugin.class); RuneLite.main(args); } } \ No newline at end of file