Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
06627d4
fix(geflipper): add profit overlay, prevent OS key leak, and fix abor…
Joinkiee Sep 6, 2026
a3c6338
fix(geflipper): remove bank item withdrawal and restrict NPC interact…
Joinkiee Sep 6, 2026
c884e65
fix(geflipper): support sub-region highlight clicks, price dialog con…
Joinkiee Sep 7, 2026
d59c93d
fix(geflipper): dynamically detect slotActionSwap and support offer s…
Joinkiee Sep 7, 2026
93e8651
feat(geflipper): automatically turn on Flipping Copilot slotActionSwa…
Joinkiee Sep 7, 2026
6cddb11
chore(geflipper): label plugin build as v1.2.5
Joinkiee Sep 11, 2026
0e557b0
feat(geflipper): add 'Auto-Enable Copilot Slot Swap' config toggle (v…
Joinkiee Sep 11, 2026
dba459f
fix(geflipper): stop false 'abort button not found' and 'offer screen…
Joinkiee Sep 12, 2026
8c5d65b
fix(geflipper): stop treating wide chatbox highlights as the GE Confi…
Joinkiee Sep 12, 2026
312a830
fix(geflipper): settle before confirming, and guard the post-login pl…
Joinkiee Sep 12, 2026
34dced2
chore(geflipper): label 1.2.51 (was 1.2.5)
Joinkiee Sep 12, 2026
64a2df7
fix(geflipper): wait for the player object before reading its locatio…
Joinkiee Sep 12, 2026
a1393f1
fix(geflipper): recover in 10s, not 30s, when the offer screen gets n…
Joinkiee Sep 12, 2026
bf47526
fix(geflipper): the Auto-Enable toggle must gate BOTH writers of slot…
Joinkiee Sep 12, 2026
6f97dba
fix(geflipper): reopen a closed Grand Exchange and escape a stray GE …
Joinkiee Sep 12, 2026
cd84b8e
fix(geflipper): address logging and modify review feedback (1.2.6)
Joinkiee Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,72 @@
@ConfigGroup("Flipper Config")
public interface FlipperConfig extends Config {

@ConfigItem(
keyName = "guide",
name = "How to use",
description = "How to use this plugin",
position = 0
enum SelectionMethod {
HOTKEY("Hotkey (E)"),
MOUSE("Mouse");

private final String name;

SelectionMethod(String name) {
this.name = name;
}

@Override
public String toString() {
return name;
}
}

/** How a slot is worked when Copilot asks for a modify or an abort. */
enum SlotAction {
COPILOT_LEFT_CLICK("On", "Copilot left-click swap"),
MENU_OPTION("Off", "Slot menu action");

private final String label;
final String actionDescription;

SlotAction(String label, String actionDescription) {
this.label = label;
this.actionDescription = actionDescription;
}

@Override
public String toString() {
return label;
}
}

@ConfigItem(
keyName = "slotActionMode",
name = "Copilot left-click swap",
description = "On: use Copilot's swapped left-click for Modify/Abort (enable slot swap in Copilot too). "
+ "Off: select the supported Modify/Abort slot action directly. "
+ "This setting controls GE Flipper and does not change Copilot's own setting. "
+ "Hotkey/Mouse above controls price and quantity input.",
position = 2
)
default SlotAction slotAction() {
return SlotAction.COPILOT_LEFT_CLICK;
}


@ConfigItem(
keyName = "verboseLogging",
name = "Verbose Logging",
description = "Log this plugin's own activity at INFO instead of WARN. Leave it off to keep "
+ "the in-game chat quiet; turn it on when you need a full trace of what the plugin did. "
+ "This affects only this plugin's own logger - no other script's logging is touched.",
position = 90
)
default boolean verboseLogging() {
return false;
}

@ConfigItem(
keyName = "guide",
name = "How to use",
description = "How to use this plugin",
position = 0
)
default String GUIDE() {
return "Automates the Flipping copilot plugin from the plugin hub, \n" +
Expand All @@ -22,5 +83,25 @@ default String GUIDE() {
"~made by chocken \n" +
"Extra tip: In game settings, disable grand exchange warnings for offers with the price too low/high, otherwise the script will get stuck.";
}


@ConfigItem(
keyName = "selectionMethod",
name = "Suggestion Selection",
description = "Choose whether to use the hotkey (E) or mouse clicks to select what Copilot suggests",
position = 1
)
default SelectionMethod selectionMethod() {
return SelectionMethod.HOTKEY;
}

@ConfigItem(
keyName = "showOverlay",
name = "Show Overlay",
description = "Display profit and GP/hr overlay on the top-left of the screen",
position = 3
)
default boolean showOverlay() {
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
package net.runelite.client.plugins.microbot.geflipper;

import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.microbot.Microbot;
import net.runelite.client.ui.overlay.OverlayPanel;
import net.runelite.client.ui.overlay.OverlayPosition;
import net.runelite.client.ui.overlay.components.LineComponent;
import net.runelite.client.ui.overlay.components.TitleComponent;

import javax.inject.Inject;
import javax.swing.JLabel;
import java.awt.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Locale;

public class FlipperOverlay extends OverlayPanel {
private static final Color POSITIVE_COLOR = new Color(0x52D273);
private static final Color NEGATIVE_COLOR = new Color(0xFF6666);
private static final Color TITLE_COLOR = Color.CYAN;

private final FlipperPlugin plugin;
private final FlipperConfig config;

private Plugin flippingCopilot;
private Object flipManager;
private Object sessionManager;
private Object statsPanel;

private String overallProfitStr = "0 gp";
private Color overallProfitColor = Color.WHITE;
private String gpHrStr = "0 gp/hr";
private Color gpHrColor = Color.WHITE;
private String sessionProfitStr = "0 gp";
private Color sessionProfitColor = Color.WHITE;
private String sessionTimeStr = null;

private long lastFetchTime = 0;

@Inject
public FlipperOverlay(FlipperPlugin plugin, FlipperConfig config) {
super(plugin);
this.plugin = plugin;
this.config = config;
setPosition(OverlayPosition.TOP_LEFT);
setNaughty();
}

private void updateCopilotStats() {
long now = System.currentTimeMillis();
if (now - lastFetchTime < 1000) return;
lastFetchTime = now;

if (flippingCopilot == null) {
flippingCopilot = Microbot.getPluginManager()
.getPlugins()
.stream()
.filter(p -> p.getClass().getSimpleName().equalsIgnoreCase("FlippingCopilotPlugin"))
.findFirst()
.orElse(null);
}
if (flippingCopilot == null) {
overallProfitStr = "Copilot not found";
overallProfitColor = Color.GRAY;
gpHrStr = "-";
gpHrColor = Color.GRAY;
return;
}

try {
if (flipManager == null) {
Field fmField = flippingCopilot.getClass().getDeclaredField("flipManager");
fmField.setAccessible(true);
flipManager = fmField.get(flippingCopilot);
}
if (sessionManager == null) {
Field smField = flippingCopilot.getClass().getDeclaredField("sessionManager");
smField.setAccessible(true);
sessionManager = smField.get(flippingCopilot);
}
if (statsPanel == null) {
try {
Field spField = flippingCopilot.getClass().getDeclaredField("statsPanel");
spField.setAccessible(true);
statsPanel = spField.get(flippingCopilot);
} catch (Exception ignored) {}
}
} catch (Exception ignored) {}

long overallProfit = 0;
long sessionProfit = 0;
long gpHr = 0;
boolean gotOverall = false;
boolean gotSession = false;

// 1. Query flipManager for exact overall & session profits
if (flipManager != null) {
try {
// calculateStats(0, null) calculates all-time stats across all accounts
Method calculateStats = flipManager.getClass().getMethod("calculateStats", int.class, Integer.class);
Object overallStats = calculateStats.invoke(flipManager, 0, null);
if (overallStats != null) {
Field profitField = overallStats.getClass().getField("profit");
overallProfit = profitField.getLong(overallStats);
gotOverall = true;
}

// getIntervalStats() gets current interval / session stats
Method getIntervalStats = flipManager.getClass().getMethod("getIntervalStats");
Object intervalStats = getIntervalStats.invoke(flipManager);
if (intervalStats != null) {
Field profitField = intervalStats.getClass().getField("profit");
sessionProfit = profitField.getLong(intervalStats);
gotSession = true;
}
} catch (Exception ignored) {}
}

// 2. Query sessionManager for runtime & compute gp/hr
if (sessionManager != null) {
try {
Method getCachedSessionData = sessionManager.getClass().getMethod("getCachedSessionData");
Object sessionData = getCachedSessionData.invoke(sessionManager);
if (sessionData != null) {
Field durationField = sessionData.getClass().getField("durationMillis");
long durationMillis = durationField.getLong(sessionData);
if (durationMillis > 0) {
double hours = durationMillis / 3600000.0;
if (hours > 0) {
gpHr = (long) (sessionProfit / hours);
}
long totalSeconds = durationMillis / 1000;
long h = totalSeconds / 3600;
long m = (totalSeconds % 3600) / 60;
long s = totalSeconds % 60;
sessionTimeStr = String.format("%02d:%02d:%02d", h, m, s);
}
}
} catch (Exception ignored) {}
}

// 3. Check statsPanel UI labels if available to supplement
if (statsPanel != null) {
try {
// If hourlyProfitVal label has Copilot's formatted text
Field hourlyField = statsPanel.getClass().getDeclaredField("hourlyProfitVal");
hourlyField.setAccessible(true);
Object hourlyObj = hourlyField.get(statsPanel);
if (hourlyObj instanceof JLabel) {
String text = ((JLabel) hourlyObj).getText();
if (text != null && !text.trim().isEmpty() && !text.equals("0 gp/hr") && gpHr == 0) {
gpHrStr = text;
gpHrColor = getColorForText(text);
}
}

// If overall wasn't found from flipManager, read totalProfitVal
if (!gotOverall) {
Field totalField = statsPanel.getClass().getDeclaredField("totalProfitVal");
totalField.setAccessible(true);
Object totalObj = totalField.get(statsPanel);
if (totalObj instanceof JLabel) {
String text = ((JLabel) totalObj).getText();
if (text != null && !text.trim().isEmpty()) {
overallProfitStr = text;
overallProfitColor = getColorForText(text);
gotOverall = true;
}
}
}

// Session time label
Field timeField = statsPanel.getClass().getDeclaredField("sessionTimeVal");
timeField.setAccessible(true);
Object timeObj = timeField.get(statsPanel);
if (timeObj instanceof JLabel) {
String text = ((JLabel) timeObj).getText();
if (text != null && !text.trim().isEmpty() && !text.equals("00:00:00")) {
sessionTimeStr = text;
}
}
} catch (Exception ignored) {}
}

if (gotOverall) {
overallProfitStr = formatProfit(overallProfit);
overallProfitColor = overallProfit > 0 ? POSITIVE_COLOR : (overallProfit < 0 ? NEGATIVE_COLOR : Color.WHITE);
}
if (gotSession) {
sessionProfitStr = formatProfit(sessionProfit);
sessionProfitColor = sessionProfit > 0 ? POSITIVE_COLOR : (sessionProfit < 0 ? NEGATIVE_COLOR : Color.WHITE);
}
if (gpHr != 0 || !gpHrStr.contains("gp/hr")) {
gpHrStr = formatGpHr(gpHr);
gpHrColor = gpHr > 0 ? POSITIVE_COLOR : (gpHr < 0 ? NEGATIVE_COLOR : Color.WHITE);
}
}

public static String formatProfit(long amount) {
String sign = amount > 0 ? "+" : (amount < 0 ? "-" : "");
long abs = Math.abs(amount);
if (abs >= 1_000_000_000L) {
return sign + String.format(Locale.ENGLISH, "%.2fB gp", abs / 1_000_000_000.0);
} else if (abs >= 1_000_000L) {
return sign + String.format(Locale.ENGLISH, "%.2fM gp", abs / 1_000_000.0);
} else if (abs >= 10_000L) {
return sign + String.format(Locale.ENGLISH, "%.1fK gp", abs / 1_000.0);
} else {
return sign + String.format(Locale.ENGLISH, "%,d gp", abs);
}
}

public static String formatGpHr(long amount) {
String sign = amount > 0 ? "+" : (amount < 0 ? "-" : "");
long abs = Math.abs(amount);
if (abs >= 1_000_000_000L) {
return sign + String.format(Locale.ENGLISH, "%.2fB gp/hr", abs / 1_000_000_000.0);
} else if (abs >= 1_000_000L) {
return sign + String.format(Locale.ENGLISH, "%.2fM gp/hr", abs / 1_000_000.0);
} else if (abs >= 10_000L) {
return sign + String.format(Locale.ENGLISH, "%.1fK gp/hr", abs / 1_000.0);
} else {
return sign + String.format(Locale.ENGLISH, "%,d gp/hr", abs);
}
}

private static Color getColorForText(String text) {
if (text.startsWith("+") || (!text.startsWith("-") && !text.startsWith("0"))) {
return POSITIVE_COLOR;
} else if (text.startsWith("-")) {
return NEGATIVE_COLOR;
}
return Color.WHITE;
}

@Override
public Dimension render(Graphics2D graphics) {
if (config != null && !config.showOverlay()) return null;
if (!Microbot.isLoggedIn()) return null;

updateCopilotStats();

panelComponent.getChildren().clear();
panelComponent.setPreferredSize(new Dimension(200, 0));

panelComponent.getChildren().add(TitleComponent.builder()
.text("Microbot Flipper v" + FlipperPlugin.version)
.color(TITLE_COLOR)
.build());

panelComponent.getChildren().add(LineComponent.builder()
.left("GP/hr:")
.right(gpHrStr)
.rightColor(gpHrColor)
.build());

panelComponent.getChildren().add(LineComponent.builder()
.left("Overall Profit:")
.right(overallProfitStr)
.rightColor(overallProfitColor)
.build());

panelComponent.getChildren().add(LineComponent.builder()
.left("Session Profit:")
.right(sessionProfitStr)
.rightColor(sessionProfitColor)
.build());

if (sessionTimeStr != null && !sessionTimeStr.isEmpty()) {
panelComponent.getChildren().add(LineComponent.builder()
.left("Session Time:")
.right(sessionTimeStr)
.rightColor(Color.LIGHT_GRAY)
.build());
}

String slotStatus = plugin.getFlipperScript() == null ? "" : plugin.getFlipperScript().getSlotActionStatus();
if (!slotStatus.isEmpty()) {
panelComponent.getChildren().add(LineComponent.builder()
.left(slotStatus).leftColor(Color.ORANGE).build());
}

return super.render(graphics);
}
}
Loading
Loading