feat: player tracker
This commit is contained in:
@@ -0,0 +1,322 @@
|
|||||||
|
# Item Information – Zusammenlegung der Item-Helper
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
`Item prices`, `Item charges` und das neue Feature `Banked items` werden unter einem gemeinsamen Helper zusammengeführt.
|
||||||
|
|
||||||
|
Empfohlener Name:
|
||||||
|
|
||||||
|
**Item information**
|
||||||
|
|
||||||
|
Alternativen: `Item details`, `Item info`.
|
||||||
|
|
||||||
|
Der Nutzer soll einen einzigen Hauptschalter haben. Die einzelnen Informationsarten bleiben per Rechtsklick separat aktivierbar.
|
||||||
|
|
||||||
|
## Aktueller Zustand
|
||||||
|
|
||||||
|
### Item prices
|
||||||
|
|
||||||
|
Implementierung: `helpers/ItemPriceOverlay.java`
|
||||||
|
|
||||||
|
- Hover-Tooltip über Inventar- und Bank-Items
|
||||||
|
- GE-Preis
|
||||||
|
- High-Alchemy-Wert
|
||||||
|
- Mengen werden berücksichtigt
|
||||||
|
- Coins und Platinum-Tokens werden ignoriert
|
||||||
|
- Optionen:
|
||||||
|
- `Show GE price`
|
||||||
|
- `Show HA price`
|
||||||
|
|
||||||
|
### Item charges
|
||||||
|
|
||||||
|
Implementierung:
|
||||||
|
|
||||||
|
- `helpers/ItemCharges.java`
|
||||||
|
- `helpers/ItemChargesOverlay.java`
|
||||||
|
|
||||||
|
- Permanente Werte direkt auf Item-Icons
|
||||||
|
- Inventar
|
||||||
|
- Ausrüstung
|
||||||
|
- optional Bank
|
||||||
|
- Unterstützt viele Charge-/Füllstandsquellen, unter anderem:
|
||||||
|
- Varbits
|
||||||
|
- Barrows
|
||||||
|
- Moon-Ausrüstung
|
||||||
|
- Blowpipe mit mehreren Werten
|
||||||
|
- Dragonfire-Schilde
|
||||||
|
- Schmuck und Tränke
|
||||||
|
- Bags, Pouches und Container
|
||||||
|
- Master Scroll Book
|
||||||
|
- weitere Spezialitems
|
||||||
|
- Optionen:
|
||||||
|
- `Show weapons and armor`
|
||||||
|
- `Show looting and coal bags`
|
||||||
|
- `Show charges in bank`
|
||||||
|
- Textgröße
|
||||||
|
- Low-Charge-Warnschwelle
|
||||||
|
- Primärfarbe
|
||||||
|
- Sekundärfarbe
|
||||||
|
- Warnfarbe
|
||||||
|
- gespeicherte Charges zurücksetzen
|
||||||
|
- Coal-Bag-Daten vergessen
|
||||||
|
|
||||||
|
## Neues Feature: Banked items
|
||||||
|
|
||||||
|
Beim Hover über ein Item soll im Tooltip zusätzlich die Bankmenge erscheinen.
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GE: 12.3k gp HA: 4.8k gp
|
||||||
|
Banked: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
|
||||||
|
- Die Bankmenge wird nur angezeigt, wenn sie bekannt ist.
|
||||||
|
- Bei unbekanntem Bestand keine `Banked: 0`-Zeile anzeigen.
|
||||||
|
- Noted und unnoted Items müssen zusammengefasst werden.
|
||||||
|
- Die Zuordnung muss über `ItemManager.canonicalize()` erfolgen.
|
||||||
|
- Bei geöffneter Bank ist der Live-Bankcontainer maßgeblich.
|
||||||
|
- Bei geschlossener Bank wird der zuletzt bekannte Bankbestand verwendet.
|
||||||
|
- Bankänderungen müssen über `ItemContainerChanged` berücksichtigt werden.
|
||||||
|
- Die bestehende Bank-/Container-Speicherung des Plugins soll wiederverwendet werden.
|
||||||
|
|
||||||
|
## Zieloberfläche
|
||||||
|
|
||||||
|
In der Helpers-Karte gibt es nur noch eine Checkbox:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Item information
|
||||||
|
```
|
||||||
|
|
||||||
|
Tooltip:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Shows item prices, banked quantities and charge information. Right-click for options.
|
||||||
|
```
|
||||||
|
|
||||||
|
Die bisherigen Checkboxen `Item prices` und `Item charges` entfallen.
|
||||||
|
|
||||||
|
## Ziel-Rechtsklick-Menü
|
||||||
|
|
||||||
|
### Informationsarten
|
||||||
|
|
||||||
|
- `Show GE price`
|
||||||
|
- `Show High Alchemy value`
|
||||||
|
- `Show banked quantity`
|
||||||
|
- `Show item charges`
|
||||||
|
|
||||||
|
### Charge-Kategorien
|
||||||
|
|
||||||
|
Nur relevant, wenn `Show item charges` aktiv ist:
|
||||||
|
|
||||||
|
- `Weapons and armor`
|
||||||
|
- `Bags and containers`
|
||||||
|
|
||||||
|
Die bestehende Option `Show charges in bank` wird in den Bereich Anzeigeorte verschoben.
|
||||||
|
|
||||||
|
### Anzeigeorte
|
||||||
|
|
||||||
|
Preise und Bankmengen sind Hover-Informationen:
|
||||||
|
|
||||||
|
- `Show in inventory`
|
||||||
|
- `Show in bank`
|
||||||
|
|
||||||
|
Charges sind Icon-Informationen:
|
||||||
|
|
||||||
|
- `Show charges in inventory`
|
||||||
|
- `Show charges in equipment`
|
||||||
|
- `Show charges in bank`
|
||||||
|
|
||||||
|
Falls eine zu große Anzahl an Optionen vermieden werden soll, kann `Show in inventory` entfallen, weil Inventar standardmäßig immer aktiv ist. Dann bleibt nur:
|
||||||
|
|
||||||
|
- `Show in bank`
|
||||||
|
- `Show charges in equipment`
|
||||||
|
|
||||||
|
### Darstellung
|
||||||
|
|
||||||
|
- `Text size`
|
||||||
|
- `Primary color`
|
||||||
|
- `Secondary color`
|
||||||
|
- `Warning color`
|
||||||
|
|
||||||
|
### Warnungen
|
||||||
|
|
||||||
|
`Low-charge warning` mit:
|
||||||
|
|
||||||
|
- `Off`
|
||||||
|
- `≤ 10`
|
||||||
|
- `≤ 50`
|
||||||
|
- `≤ 100`
|
||||||
|
- `≤ 500`
|
||||||
|
- `≤ 1000`
|
||||||
|
|
||||||
|
### Datenverwaltung
|
||||||
|
|
||||||
|
- `Reset saved charges`
|
||||||
|
- `Forget coal bag`
|
||||||
|
|
||||||
|
### Zurücksetzen
|
||||||
|
|
||||||
|
- `Reset appearance`
|
||||||
|
|
||||||
|
## Tooltip-Design
|
||||||
|
|
||||||
|
Der bestehende Preis-Tooltip soll zu einem gemeinsamen Tooltip-Builder erweitert werden.
|
||||||
|
|
||||||
|
Empfohlene Reihenfolge:
|
||||||
|
|
||||||
|
1. GE-Preis
|
||||||
|
2. High-Alchemy-Wert
|
||||||
|
3. Bankmenge
|
||||||
|
|
||||||
|
Beispiel mit allen Informationen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GE: 12.3k gp HA: 4.8k gp
|
||||||
|
Banked: 300
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn nur einzelne Informationen aktiviert sind, werden nur diese Zeilen ausgegeben.
|
||||||
|
|
||||||
|
Die Charge-Werte bleiben auf den Item-Icons und werden nicht in den Hover-Tooltip verschoben, außer dies wird später ausdrücklich gewünscht.
|
||||||
|
|
||||||
|
## Empfohlene interne Struktur
|
||||||
|
|
||||||
|
### Gemeinsamer Helper-Schalter
|
||||||
|
|
||||||
|
Neuer Hauptschlüssel:
|
||||||
|
|
||||||
|
```text
|
||||||
|
itemInformation
|
||||||
|
```
|
||||||
|
|
||||||
|
Mögliche neue Unteroptionen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
itemInformationGe
|
||||||
|
itemInformationHa
|
||||||
|
itemInformationBanked
|
||||||
|
itemInformationCharges
|
||||||
|
itemInformationChargesWeapons
|
||||||
|
itemInformationChargesBags
|
||||||
|
itemInformationBank
|
||||||
|
itemInformationEquipment
|
||||||
|
itemInformationSize
|
||||||
|
itemInformationColor
|
||||||
|
itemInformationSecondaryColor
|
||||||
|
itemInformationLowColor
|
||||||
|
itemInformationLowAt
|
||||||
|
```
|
||||||
|
|
||||||
|
Die genaue Benennung kann noch angepasst werden. Wichtig ist ein einheitliches Präfix.
|
||||||
|
|
||||||
|
### Overlay-Struktur
|
||||||
|
|
||||||
|
Empfohlene Varianten:
|
||||||
|
|
||||||
|
1. `ItemInformationOverlay` übernimmt den Hover-Tooltip und ruft Preis- und Banked-Provider auf.
|
||||||
|
2. `ItemChargesOverlay` bleibt für die Icon-Anzeige bestehen.
|
||||||
|
3. Beide verwenden denselben gemeinsamen Hauptschalter und dieselben zentralen Optionen.
|
||||||
|
|
||||||
|
Damit muss die komplexe Charge-Logik nicht unnötig mit der Tooltip-Logik vermischt werden.
|
||||||
|
|
||||||
|
Alternativ kann der bestehende `ItemPriceOverlay` umbenannt und erweitert werden. Inhaltlich sollte er aber als allgemeiner Item-Information-Tooltip behandelt werden.
|
||||||
|
|
||||||
|
### Bankbestand
|
||||||
|
|
||||||
|
Für den Bankbestand sollte keine neue parallele Speicherung eingeführt werden, wenn die vorhandenen Plugin-Daten verwendet werden können.
|
||||||
|
|
||||||
|
Benötigt wird eine Methode nach diesem Muster:
|
||||||
|
|
||||||
|
```java
|
||||||
|
long getBankedQuantity(int itemId)
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Methode sollte:
|
||||||
|
|
||||||
|
- die Item-ID kanonisieren
|
||||||
|
- noted/unnoted zusammenführen
|
||||||
|
- die aktuelle Bank bevorzugen, wenn sie geöffnet ist
|
||||||
|
- ansonsten den letzten bekannten Bestand verwenden
|
||||||
|
- bei unbekanntem Item `0` oder einen klar unterscheidbaren „unknown“-Zustand liefern
|
||||||
|
|
||||||
|
Wichtig: `0` darf nicht automatisch als „unbekannt“ interpretiert werden. Ein bekannter leerer Bestand und ein unbekannter Bestand müssen unterscheidbar sein.
|
||||||
|
|
||||||
|
## Rückwärtskompatibilität
|
||||||
|
|
||||||
|
Bestehende Konfigurationswerte sollen zunächst gelesen werden können:
|
||||||
|
|
||||||
|
- `itemPrices`
|
||||||
|
- `itemPricesGe`
|
||||||
|
- `itemPricesHa`
|
||||||
|
- `itemCharges`
|
||||||
|
- `itemChargesWeapons`
|
||||||
|
- `itemChargesBags`
|
||||||
|
- `itemChargesBank`
|
||||||
|
- `itemChargesSize`
|
||||||
|
- `itemChargesColor`
|
||||||
|
- `itemChargesSecondaryColor`
|
||||||
|
- `itemChargesLowColor`
|
||||||
|
- `itemChargesLowAt`
|
||||||
|
|
||||||
|
Mögliche Strategie:
|
||||||
|
|
||||||
|
- Neuer Hauptschalter verwendet neue Keys.
|
||||||
|
- Falls neue Keys noch nicht existieren, werden die alten Werte als Fallback gelesen.
|
||||||
|
- Alte Einstellungen werden nicht sofort gelöscht.
|
||||||
|
- Ein späterer Migrationsschritt kann alte Keys übernehmen oder dauerhaft weiter unterstützen.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Vor der Implementierung sollten die vorhandenen Tests ausgeführt werden. Relevant sind insbesondere:
|
||||||
|
|
||||||
|
- `ItemChargesTest`
|
||||||
|
- `ItemChargesFullTest`
|
||||||
|
- Bank-/Container-Tests
|
||||||
|
- Tests für `ItemManager.canonicalize()`-Verhalten, falls vorhanden
|
||||||
|
|
||||||
|
Neue Tests sollten abdecken:
|
||||||
|
|
||||||
|
1. GE-Preis allein
|
||||||
|
2. HA-Wert allein
|
||||||
|
3. GE und HA zusammen
|
||||||
|
4. Bankmenge bei normalem Item
|
||||||
|
5. Bankmenge bei noted/unnoted Kombination
|
||||||
|
6. unbekannter Bankbestand wird nicht angezeigt
|
||||||
|
7. bekannter Bankbestand von `0`
|
||||||
|
8. Bankbestand aktualisiert sich nach Containeränderung
|
||||||
|
9. gemeinsamer Hauptschalter deaktiviert beide Overlay-Arten
|
||||||
|
10. Charge-Unteroptionen bleiben funktionsfähig
|
||||||
|
11. alte Konfigurationswerte werden korrekt übernommen
|
||||||
|
12. Tooltip erscheint nicht bei Coins/Platinum, sofern das bestehende Verhalten beibehalten werden soll
|
||||||
|
|
||||||
|
## Offene Fragen vor der Implementierung
|
||||||
|
|
||||||
|
1. Soll der endgültige Name `Item information`, `Item details` oder `Item info` sein?
|
||||||
|
2. Soll `Banked: 0` bei bekannt leerem Bestand angezeigt werden oder komplett entfallen?
|
||||||
|
3. Soll der Bankbestand auch beim Hover über ein Item im Bankfenster selbst angezeigt werden?
|
||||||
|
4. Soll die Bankmenge inklusive Items im Inventar angezeigt werden oder ausschließlich der Bestand in der Bank?
|
||||||
|
5. Soll `Show in inventory` entfallen, weil Inventar immer aktiv sein soll?
|
||||||
|
6. Sollen Preise und Bankmenge optional auch auf Item-Icons statt nur im Tooltip erscheinen?
|
||||||
|
7. Soll die Anzeige von Charges im Bankfenster weiterhin separat schaltbar sein?
|
||||||
|
8. Soll eine gemeinsame Farbe für alle Tooltip-Zeilen verwendet werden oder behalten GE/HA/Banked eigene Farben?
|
||||||
|
9. Sollen die alten Config-Keys dauerhaft unterstützt oder nur einmalig migriert werden?
|
||||||
|
10. Soll das neue Feature standardmäßig deaktiviert bleiben, wie die bisherigen Helper?
|
||||||
|
|
||||||
|
## Empfohlene Standardentscheidung
|
||||||
|
|
||||||
|
Für eine einfache und übersichtliche erste Version:
|
||||||
|
|
||||||
|
- Name: `Item information`
|
||||||
|
- Ein Hauptschalter
|
||||||
|
- GE, HA, Banked und Charges separat toggelbar
|
||||||
|
- Inventar immer aktiv
|
||||||
|
- Bank-Hover separat toggelbar
|
||||||
|
- Equipment-Charges separat toggelbar
|
||||||
|
- Banked zählt nur Bankbestand, nicht Inventar
|
||||||
|
- unbekannter Bestand wird nicht angezeigt
|
||||||
|
- vorhandene Charge-Unteroptionen bleiben erhalten
|
||||||
|
- alte Config-Keys werden als Fallback unterstützt
|
||||||
|
- Tooltip-Informationen bleiben getrennt von den Charge-Zahlen auf Item-Icons
|
||||||
-136
@@ -1,136 +0,0 @@
|
|||||||
# Item charges – Taskbeschreibung
|
|
||||||
|
|
||||||
Implementiere einen **„Item charges"**-Helper in **Essentials Plus** (`com.example.essentialsplus.helpers`): Auf dem **Icon jedes Items** mit bekanntem Charge/Füllstand wird eine **Zahl** gerendert. Manche Items zeigen **mehrere Zahlen** (z. B. Toxic Blowpipe: Scales + Darts). Togglebar per Checkbox, anzeige in Inventar, Ausrüstung und (optional) Bank.
|
|
||||||
|
|
||||||
**Beispiele:**
|
|
||||||
- Toxic Blowpipe (mit Scales) → `scales`-Zahl; mit geladenen Darts → zweiter Wert (Darts-Marker, s. Hinweise).
|
|
||||||
- Dizan's Quiver → zwei Zahlen: Ammo (`CHARGES_DIZANAS_QUIVER_QUANTITY`) + Blessing (`CHARGES_DIZANAS_QUIVER_BLESSING`).
|
|
||||||
- Trident/Shadow/Tome/Bowfa etc. → eine Zahl.
|
|
||||||
- **Lootingsack**: Geldwert (Coins) als Zahl auf dem Sack-Icon.
|
|
||||||
- **Coal Bag**: Füllstand via Chat-Tracking + Persistenz (kein Varbit!).
|
|
||||||
|
|
||||||
**Format:** Ganzzahlen / Stack-Format (Geld: `QuantityFormatter.quantityToStackSize`, z. B. `1.2M`). Unbekannter Wert → `?`.
|
|
||||||
|
|
||||||
## Datenquellen & Mechanik
|
|
||||||
|
|
||||||
### 1. `CHARGES_*`-Varbits (Hauptquelle, charakterweit)
|
|
||||||
|
|
||||||
Werte via `client.getVarbitValue(id)` (`net.runelite.api.gameval.VarbitID`). Mapping Item-Gruppe → Varbit (verifiziert, Release 1.12.39). Varbit aus der **ID-Variante** des Items ableiten (s. Quelle 2), nicht aus einem Katalog — zwei identische Waffen haben nur EINEN Varbit-Wert (OSRS-Grenze!).
|
|
||||||
|
|
||||||
| Item-Gruppe (ItemID-Konstanten) | VarbitID-Konstante |
|
|
||||||
|---|---|
|
|
||||||
| `TOXIC_BLOWPIPE`, `TOXIC_BLOWPIPE_LOADED`, `TOXIC_BLOWPIPE_ORNAMENT`, `TOXIC_BLOWPIPE_LOADED_ORNAMENT` | `CHARGES_TOXIC_BLOWPIPE_QUANTITY` |
|
|
||||||
| `SERPENTINE_HELM`, `SERPENTINE_HELM_CHARGED`, `_CYAN`, `_CYAN_CHARGED`, `_RED`, `_RED_CHARGED` | `CHARGES_SERPENTINE_HELM_QUANTITY` |
|
|
||||||
| `WARPED_SCEPTRE`, `WARPED_SCEPTRE_UNCHARGED` | `CHARGES_WARPED_SCEPTRE_QUANTITY` |
|
|
||||||
| `TUMEKENS_SHADOW`, `TUMEKENS_SHADOW_UNCHARGED`, `TUMEKENS_HEKA`, `TUMEKENS_HEKA_UNCHARGED` | `CHARGES_TUMEKENS_SHADOW_QUANTITY` |
|
|
||||||
| `SANGUINESTI_STAFF`, `SANGUINESTI_STAFF_UNCHARGED`, `_OR`, `_UNCHARGED_OR` | `CHARGES_SANGUINESTI_STAFF_QUANTITY` |
|
|
||||||
| `TRIDENT_OF_THE_SEAS` (+ `_E`), jew. gecharge/un-gecharge-Varianten | `CHARGES_TRIDENT_OF_THE_SEAS_QUANTITY` bzw. `..._E_QUANTITY` |
|
|
||||||
| `TOXIC_STAFF_OF_THE_DEAD` (+ `_UNCHARGED`, `_OR`-Varianten) | `CHARGES_TOXIC_STAFF_OF_THE_DEAD_QUANTITY` |
|
|
||||||
| `BOW_OF_FAERDHINEN`, `BOW_OF_FAERDHINEN_INACTIVE`, `_INFINITE*` | `CHARGES_BOW_OF_FAERDHINEN_QUANTITY` (+ `CHARGES_BOW_OF_FAERDHINEN_CORRUPTION` für korrupte Variante) |
|
|
||||||
| `BLADE_OF_SAELDOR`, `BLADE_OF_SAELDOR_INACTIVE`, `_INFINITE` | `CHARGES_BLADE_OF_SAELDOR_QUANTITY` (+ `_CORRUPTION`) |
|
|
||||||
| `VENATOR_BOW`, `VENATOR_BOW_UNCHARGED`, `VENATOR_BOW_ORNAMENT(_UNCHARGED)` | `CHARGES_VENATOR_BOW_QUANTITY` |
|
|
||||||
| `TONALZTICS_OF_RALOS_CHARGED`, `_UNCHARGED` | `CHARGES_TONALZTICS_OF_RALOS_QUANTITY` |
|
|
||||||
| `ARCLIGHT` | `CHARGES_ARCLIGHT_QUANTITY` |
|
|
||||||
| `DIZANAS_QUIVER_CHARGED`, `_UNCHARGED`, `_INFINITE`, `_BROKEN`, Trouver-Varianten | `CHARGES_DIZANAS_QUIVER_QUANTITY` (Ammo) + `CHARGES_DIZANAS_QUIVER_BLESSING` (Blessing) — zwei Zahlen |
|
|
||||||
| `TOME_OF_FIRE`, `TOME_OF_FIRE_UNCHARGED` | `CHARGES_TOME_OF_FIRE_QUANTITY` |
|
|
||||||
| `TOME_OF_WATER`, `TOME_OF_WATER_UNCHARGED` | `CHARGES_TOME_OF_WATER_QUANTITY` |
|
|
||||||
| `TOME_OF_EARTH`, `TOME_OF_EARTH_UNCHARGED` | `CHARGES_TOME_OF_EARTH_QUANTITY` |
|
|
||||||
| `ASH_SANCTIFIER` | `CHARGES_ASH_SANCTIFIER_QUANTITY` |
|
|
||||||
| `BONECRUSHER` | `CHARGES_BONECRUSHER_QUANTITY` |
|
|
||||||
| `BRYOPHYTAS_STAFF` | `CHARGES_BRYOPHYTAS_STAFF_QUANTITY` |
|
|
||||||
| `CIRCLET_OF_WATER` | `CHARGES_CIRCLET_OF_WATER_QUANTITY` |
|
|
||||||
| `SOUL_BEARER` | `CHARGES_SOUL_BEARER_QUANTITY` |
|
|
||||||
| `CELESTIAL_RING`, `CELESTIAL_RING_CHARGED` | `CHARGES_CELESTIAL_RING_QUANTITY` |
|
|
||||||
| `WILD_CAVE_SCEPTRE_CHARGED`, `WILD_CAVE_SCEPTRE_UNCHARGED` | `CHARGES_WILDERNESS_WEAPON_QUANTITY` |
|
|
||||||
| `BRACELET_OF_ETHEREUM` | `CHARGES_BRACELET_OF_ETHEREUM_QUANTITY` |
|
|
||||||
| `RING_OF_SUFFERING` | `CHARGES_RING_OF_SUFFERING_QUANTITY` |
|
|
||||||
| `XERIC_TALISMAN`, `XERIC_TALISMAN_EMPTY` | `CHARGES_XERICS_TALISMAN_QUANTITY` |
|
|
||||||
| `RING_OF_SHADOWS`, `RING_OF_SHADOWS_UNCHARGED` | `CHARGES_RING_OF_SHADOWS_QUANTITY` |
|
|
||||||
| `ECHO_BOOTS` | `CHARGES_ECHO_BOOTS_QUANTITY` |
|
|
||||||
| `AMULET_OF_BLOOD_FURY` | `CHARGES_AMULET_OF_BLOOD_FURY_QUANTITY` |
|
|
||||||
| `GIANTSOUL_AMULET_CHARGED`, `_UNCHARGED` | `CHARGES_GIANTSOUL_AMULET_QUANTITY` |
|
|
||||||
| `EYE_OF_AYAK`, `EYE_OF_AYAK_UNCHARGED` | `CHARGES_EYE_OF_AYAK_QUANTITY` |
|
|
||||||
| `HORN_OF_PLENTY`, `_UNCHARGED`, `SAILORS_AMULET`, `_EMPTY`, `COWBELL_AMULET`, `_EMPTY`, `PENDANT_OF_ATES`, `_EMPTY` | jew. `CHARGES_*_QUANTITY`-Varbit |
|
|
||||||
| Crystal Armour/Tools (Crystal, `CRYSTAL_*` piece IDs) | `CHARGES_CRYSTAL_ARMOUR_QUANTITY`, `CHARGES_CRYSTAL_TOOLS_QUANTITY`, `CHARGES_CRYSTAL_WEAK_QUANTITY` |
|
|
||||||
| `ALCHEMIST_RING` | `CHARGES_ALCHEMIST_AMULET_QUANTITY` |
|
|
||||||
|
|
||||||
Vollständige Liste ergänzen per `javap -constants net.runelite.api.gameval.VarbitID | grep 'CHARGES_.*_QUANTITY'` gegen das runelite-api-Jar (release 1.12.39; Pfad s. **Referenzen**). Das sind ~55 Konstanten; es kommen auch Solo-/Sailing-Charges vor (`CHARGES_PLANK_SACK*`, `CHARGES_BOWSTRING_SPOOL*`), nur relevante übernehmen. Stichproben-Hinweis: `DBTableID$Charges$Row` (in runelite-api `net.runelite.api.gameval.DBTableID`) listet dieselben Charge-Typen als DB-Rows (Referenz/Querschnitt, nicht zwingend zur Laufzeit nötig).
|
|
||||||
|
|
||||||
Varianten-Ableitung für die Tabelle: Einträge in `net.runelite.api.gameval.ItemID` mit typischen Suffixen (`_CHARGED`, `_UNCHARGED`, `_INACTIVE`, `_EMPTY`, `_LOADED`, `_OR`, `_CYAN/_RED`, `_TROUVER`, `_INFINITE`, `_BROKEN`) — per `javap -constants ... ItemID | grep` verifizieren.
|
|
||||||
|
|
||||||
### 2. Item-ID-Statusvarianten (Ladungs-Status)
|
|
||||||
|
|
||||||
Viele Items wechseln die **Item-ID** bei leer/laden — mehrere exakte IDs auf dieselbe Charge abbilden bzw. „0"-Semantik:
|
|
||||||
|
|
||||||
- `TOXIC_BLOWPIPE` (12924) vs `TOXIC_BLOWPIPE_LOADED` (12926): **Darts geladen: ja/nein** (zweiter Wert, Marker; die exakte **Dart-Anzahl ist NICHT in der API** — kein Varbit/Container). `DRYGORE_BLOWPIPE` vs `DRYGORE_BLOWPIPE_LOADED` analog.
|
|
||||||
- `BOW_OF_FAERDHINEN_INACTIVE` → 0; `BLADE_OF_SAELDOR_INACTIVE` → 0; `*_INFINITE` → „∞".
|
|
||||||
- Alle `*_UNCHARGED`/`*_EMPTY` → 0 bzw. kein Varbit abfragen.
|
|
||||||
|
|
||||||
### 3. Container (Echtzeit; per GameTick gelesen)
|
|
||||||
|
|
||||||
- **Lootingsack**: `client.getItemContainer(InventoryID.LOOTING_BAG)` (auch `LOOTING_BAG_OPEN`). Geldwert = Σ qty von `ItemID.COINS` im Container. (Plugin hat bereits `readContainer()`/WealthStore für Loot — hier eigenes Lesen im Overlay-Tick, da das Overlay nicht an den Container-Event gebunden ist.)
|
|
||||||
- **Dizan's Quiver Ammo**: `InventoryID.DIZANAS_QUIVER_AMMO` als Crosscheck/Geld-Anzeige zum Varbit-Wert möglich.
|
|
||||||
- **Coal/Gem/Herb sack**: kein Container, kein Varbit (verifiziert) → Quelle 4.
|
|
||||||
|
|
||||||
### 4. Coal Bag — Chat-Tracking + Persistenz
|
|
||||||
|
|
||||||
- Kein Varbit/Container in `VarbitID`/`InventoryID` (verifiziert; nur `ItemID.COAL_BAG`/`COAL_BAG_OPEN`).
|
|
||||||
- Füllstand (max **27**) ableiten aus **Chat-Nachrichten** + **letzten bekannten Wert pro Account** in `configManager` persistieren (key `coalBag.<accountKey>`, Wert int 0–27; `accountKey`-Konvention wie bestehende Persistenz im Plugin).
|
|
||||||
- Zu verarbeitende Muster (ChatMessage-Filter `Game`): Check-/Fill-Feedback „[…]`X` coal in your coal bag]" o. Ä. *exakt in-game verifizieren* (2024 wurde Feedback von Popups auf Chat umgestellt); zusätzlich Empty → 0; bei unbekannter Nachricht/Login ohne Check → **`?`** anzeigen statt falscher Zahl.
|
|
||||||
- `ChatMessage`-Events: Fehler-tolerant parsen (Regex), bei Update sofort speichern. Beim `LOGOUT` aktuellen Wert speichern. Optional Rechtsklick „Coal bag vergessen" → Wert nicht aus Chat-Nachrichten ableiten.
|
|
||||||
|
|
||||||
## Neue Dateien
|
|
||||||
|
|
||||||
### `helpers/ItemCharges.java` (`@Singleton`, Logik)
|
|
||||||
|
|
||||||
- **Datenmodell**: `ChargeValue { String text; Color color; }` (+ optional 2. Wert). Lookup-API `List<ChargeValue> chargesFor(int itemId)`:
|
|
||||||
1. exakte `itemId` in Tabelle (Load/Empty/Inactive-Varianten); dann
|
|
||||||
2. `itemManager.canonicalize(itemId)` (Noten/Placeholder) erneut prüfen.
|
|
||||||
- **`CHARGE_MAP`**: `Map<Integer itemId, ChargeSource>` statisch aufgebaut aus den ItemID-Konstanten (Quelle 1+2); `ChargeSource` = Varbit-Konstante bzw. `COAL_BAG`-Typ bzw. `LOOTING_BAG`-Container-Typ.
|
|
||||||
- **State**: Coal-Bag-Count pro Account + `Map<Integer, ...>`-Cache nur so weit nötig (Varbit-Lesen ist billig; kein harter Cache zwingend).
|
|
||||||
- **Events** (im Plugin registriert, Muster `TreeCooldowns`):
|
|
||||||
- `GameTick` → die zwei Bag-Varbits/Container aktualisieren.
|
|
||||||
- `VarbitChanged` → relevante Varbit-Id → Value merken.
|
|
||||||
- `ChatMessage` → Coal-Bag-Messages parsen (s. o.), speichern.
|
|
||||||
- `GameStateChanged` → `!= LOGGED_IN` → `reset()`, Coal-Bag-Count aus `configManager` laden bzw. auf „?" zurücksetzen.
|
|
||||||
- **Reset**: Coal-Bag-Cache auf `?` beim Logout (Persistenz bleibt).
|
|
||||||
|
|
||||||
### `helpers/ItemChargesOverlay.java` (`WidgetItemOverlay`)
|
|
||||||
|
|
||||||
- `showOnInventory(); showOnEquipment(); showOnBank();` — Bank über Config-Sub-Key steuerbar (s. unten).
|
|
||||||
- `renderItemOverlay(g, itemId, WidgetItem)`:
|
|
||||||
- `if (!config.itemCharges()) return;`
|
|
||||||
- `List<ChargeValue>` aus `chargesFor(itemId)`; leer → nichts rendern.
|
|
||||||
- Zahl unten-links am Icon (offizielles RuneLite-Muster): `bounds.x - 1, bounds.y + 15`, mit Schatten (`Fonts.RUNESCAPE_SMALL` bzw. `TimerTextSize`), Farbe aus `itemChargesColor`; **2. Wert** (Blowpipe-Darts, Quiver-Blessing) darüber (`bounds.y - 3`) in `itemChargesSecondaryColor`.
|
|
||||||
- Geld (Lootingsack): Stack-Size-Format; Low-Charge-Färbung ab `itemChargesLowAt` → `itemChargesLowColor`.
|
|
||||||
- Coins/Loot-Bag-Wert: aus `chargesFor(LOOTING_BAG_OPEN/LOOTING_BAG)`-Container-Lesung im Overlay-Tick (Falls Container null → kein Wert).
|
|
||||||
|
|
||||||
## Zu ändernde Dateien
|
|
||||||
|
|
||||||
- **`EssentialsPlusConfig.java`**: `@ConfigItem itemCharges` (bool, default `false`, position ~37). Optionen als `configManager`-Keys via `setHelperOption` (Muster wie treeCooldowns):
|
|
||||||
- `itemChargesWeapons` (bool, true) — Varbit-Charges an/aus
|
|
||||||
- `itemChargesBags` (bool, true) — Loot-Bag-Geld + Coal-Bag-Füllstand an/aus
|
|
||||||
- `itemChargesBank` (bool, false) — Overlay auch in Bank (Achtung Hinweise!)
|
|
||||||
- `itemChargesSize` (`TimerTextSize`, SMALL), `itemChargesColor` (Color, weiß), `itemChargesSecondaryColor` (Color, hellgrau `0xC0C0C0`), `itemChargesLowColor` (Color, rot `0xFF3333`), `itemChargesLowAt` (int, 0 = aus)
|
|
||||||
- **`EssentialsPlusPlugin.java`**: `@Inject ItemCharges` + `ItemChargesOverlay`; `startUp()`: `eventBus.register(itemCharges)`, `overlayManager.add(itemChargesOverlay)`; `shutDown()`: spiegeln + `itemCharges.reset()`. `isHelperOption()`: `case "itemCharges"` → `config.itemCharges()`. `setHelperOption`/`onConfigChanged`: Bestände für `itemcharges*`-Keys refresh (Muster solcher Bestands-Abschnitte).
|
|
||||||
- **`HelpersCard.java`**: neues `JCheckBox "Item charges"` (Tooltip: „Charge-Zahl auf Item-Icons (Blowpipe, Trident, Tome, …) + Loot-Bag-Geld & Coal-Bag-Füllstand. Rechtsklick für Optionen"). Rechtsklick-Menü analog `styleMenu`/`treeMenu`: Toggles `itemChargesWeapons`/`itemChargesBags`/`itemChargesBank`, Größe, Farben, Schwellenwert, Reset. `refresh()` erweitern.
|
|
||||||
|
|
||||||
## Verifikation
|
|
||||||
|
|
||||||
- `./gradlew compileJava` (bzw. `./gradlew deploy` für Bolt).
|
|
||||||
- Manuell: Blowpipe mit Scales (nur Scales-Zahl) → mit Darts laden (2. Zahl erscheint); Trident/Shadow/Sang/Tome (Zahl); Lootingsack mit Coins betreten (Geld auf Sack-Icon); Coal Bag füllen → Zahl steigt, Empty → 0, Relog → Wert bleibt; Bank öffnen mit `itemChargesBank` → Zahlen auch dort; beide Blowpipes (leer + geladen) im Inventar → korrekte per-ID-Darstellung.
|
|
||||||
|
|
||||||
## Hinweise / Einschränkungen
|
|
||||||
|
|
||||||
- `CHARGES_*`-Varbits sind **charakterweit, nicht pro Item-Exemplar**: zwei unterschiedlich geladene Tridents zeigen denselben Wert (OSRS-Systemgrenze, deckungsgleich mit dem In-Game-Charge-Overlay).
|
|
||||||
- **Bank-Anzeige** dementsprechend unzuverlässig für „unbenutzte" Kopien; default daher `itemChargesBank = false`.
|
|
||||||
- Coal Bag: nur Heuristik (Chat + Persistenz), kein API-Wert; nach Login unbekannt → `?`.
|
|
||||||
- Blowpipe: Dart-**Anzahl** nicht in der API → 2. Wert ist Darts-geladen-Marker, keine Zahl.
|
|
||||||
- Keine Chat-Popup-abhängigkeit (Popup = ist abgeschaltet hat), ausschließlich ChatMessage-Events verwenden.
|
|
||||||
|
|
||||||
## Referenzen
|
|
||||||
|
|
||||||
- `tree-cooldown.md` (gleiche Struktur), `reflect.txt`/Cachierte Jars für Konstanten:
|
|
||||||
- runelite-api: `/home/hikyu/.gradle/caches/modules-2/files-2.1/net.runelite/runelite-api/1.12.39/<hash>/runelite-api-1.12.39.jar` → `net/runelite/api/gameval/{VarbitID,ItemID,InventoryID,DBTableID$Charges$Row}`
|
|
||||||
- client: `/home/hikyu/.gradle/caches/modules-2/files-2.1/net.runelite/client/1.12.39/<hash>/client-1.12.39.jar` → `net.runelite.client.ui.overlay.WidgetItemOverlay`, `components.TextComponent`, `ui.FontManager`
|
|
||||||
- Konventionen: `helpers/TreeCooldowns.java` (Logik+Events), `helpers/TreeCooldownOverlay.java`/`ItemPriceOverlay.java` (Overlay-Zeichnung), `HelpersCard.java` (Checkbox+Menü), `EssentialsPlusPlugin.isHelperOption/setHelperOption/onConfigChanged`.
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
# Skill "Reset Hourly" – Taskbeschreibung
|
|
||||||
|
|
||||||
Implementiere ein **Rechtsklick-Feature „Reset Hourly"** in **Essentials Plus**. Es erscheint an zwei Stellen:
|
|
||||||
|
|
||||||
1. **In-Game-XP-HUD** (`skills/SkillsHudOverlay.java`): Rechtsklick auf das Overlay zeigt den Menüeintrag „Reset Hourly".
|
|
||||||
2. **Skills-Karte** im Seitenpanel: auf der **Overview-Karte** als Eintrag im bestehenden Karten-Rechtsklick-Menü und im **Skills-Tab** als eigenes Rechtsklick-Popup auf der Karte.
|
|
||||||
|
|
||||||
Klick setzt die **aktuell angezeigte XP-Zählung** zurück. Es wirkt **nur**, wenn der global aktive Zeitraum `Session` oder `Now` ist. Alle anderen Zeiträume (`1H`, `1D`, `2W`, `3M`, `All`) bleiben unberührt – insbesondere bleibt der **Gesamt-XP-Wert (`All`) erhalten**, weil er aus der ersten Sample-Spur abgeleitet wird.
|
|
||||||
|
|
||||||
**Display-Sprache:** Deutsch (Menütext trotzdem exakt `Reset Hourly`).
|
|
||||||
|
|
||||||
## Hintergrund / Datenmodell
|
|
||||||
|
|
||||||
- `skills/SkillHistory.java` speichert **absolute** XP-Samples pro Skill mit Zeitstempel (`XpSample`, `List<XpSample> samples`), ein File pro Charakter (JSON in `essentials-plus/skills`).
|
|
||||||
- `ui/TimeFilter.java` + `ui/TimeRange.java` liefern den global geteilten Zeitraum: `CURRENT("Now")`, `SESSION("Sess")`, `HOUR("1H")`, `DAY`, `TWO_WEEKS`, `QUARTER`, `ALL`.
|
|
||||||
- Gewinne werden als `latest - at(start)` berechnet; der Start kommt aus `SkillHistory.start(filter)`, `starts(filter)` und `gained(filter)`.
|
|
||||||
- Es gibt **noch kein** Reset-/Baseline-Feld. Der Reset wird als **Zeitmarker** umgesetzt, nicht durch Löschen von Samples.
|
|
||||||
|
|
||||||
## Neue Dateien
|
|
||||||
|
|
||||||
### `skills/SkillHistory.java` (erweitern, keine neue Datei)
|
|
||||||
|
|
||||||
- Zwei persistente Marker ergänzen (0 = nie zurückgesetzt):
|
|
||||||
- `public long resetSessionAt;`
|
|
||||||
- `public long resetNowAt;`
|
|
||||||
- `public synchronized void reset(TimeRange range)`:
|
|
||||||
- `SESSION` → `resetSessionAt = System.currentTimeMillis()`
|
|
||||||
- `CURRENT` → `resetNowAt = System.currentTimeMillis()`
|
|
||||||
- sonst → no-op
|
|
||||||
- `dirty = true` (Persistenz-Flag, existiert bereits)
|
|
||||||
- Startlogik **nur** für die betroffenen Ranges clampen:
|
|
||||||
- `starts(TimeFilter)`: bei `SESSION` für jeden Skill `Math.max(filter.start(), resetSessionAt)`; bei `CURRENT` pro Skill `Math.max(currentStart(i, now), resetNowAt)`, idle (`Long.MAX_VALUE`) bleibt idle.
|
|
||||||
- `start(TimeFilter)`: bei `SESSION` `Math.max(filter.start(), resetSessionAt)`; bei `CURRENT` nach `currentStart(-1, now)` clampen (Fallback `now` wenn idle).
|
|
||||||
- `gained(TimeFilter)`: Nicht-Current-Zweig von `gained(filter.start())` auf `gained(start(filter))` umstellen, damit der Session-Clamp greift. Current-Zweig nutzt bereits `starts(filter)`.
|
|
||||||
- Da `range(long)`, `levelUps(long)` und `activeMillis(long, int)` ihren Start aus `start(filter)` bekommen, ziehen Charts, Stats/„XP per hour" und Level-Ups automatisch mit.
|
|
||||||
- `normalize()` prüft nur `skills`/`samples`; die neuen Felder brauchen keine Migration (fehlend = 0 = nie zurückgesetzt).
|
|
||||||
|
|
||||||
### `skills/SkillStore.java` (erweitern)
|
|
||||||
|
|
||||||
- `public void resetHourly(String key, TimeRange range)`: History per `histories.get(key)` holen; wenn vorhanden `h.reset(range)` und `save(h)` aufrufen.
|
|
||||||
- Wird vom Plugin auf dem Executor aufgerufen (File-I/O), nicht auf dem Swing-/Client-Thread.
|
|
||||||
|
|
||||||
## Zu ändernde Dateien
|
|
||||||
|
|
||||||
### `skills/SkillsHudOverlay.java`
|
|
||||||
|
|
||||||
- Im Konstruktor: `addMenuEntry(net.runelite.api.MenuAction.RUNELITE_OVERLAY, "Reset Hourly", "Skills")`.
|
|
||||||
- Die Render-Logik nutzt bereits `history.starts(filter)`/`gained(filter)` → greift den Reset automatisch, **kein** Render-Umbau nötig.
|
|
||||||
- Verifikation der API (RuneLite 1.12.39): `Overlay.addMenuEntry(MenuAction, option, target)` und die 4-arg-Variante mit `Consumer<MenuEntry>` existieren; `OverlayRenderer` injiziert den Callback in `MenuEntry.onClick(...)` **und** feuert `OverlayMenuClicked`.
|
|
||||||
|
|
||||||
### `EssentialsPlusPlugin.java`
|
|
||||||
|
|
||||||
- Import `net.runelite.client.events.OverlayMenuClicked` (+ `net.runelite.client.ui.overlay.Overlay` falls nötig).
|
|
||||||
- `public void resetSkillsHourly(String key)`:
|
|
||||||
- `if (key == null) return;`
|
|
||||||
- `executor.execute(() -> skillStore.resetHourly(key, timeFilter.getRange()));`
|
|
||||||
- danach Panel-Refresh: `EssentialsPlusPanel p = panel; if (p != null) p.refreshSkillsLater();`
|
|
||||||
- `@Subscribe public void onOverlayMenuClicked(OverlayMenuClicked event)`:
|
|
||||||
- wenn `event.getOverlay() == skillsHudOverlay` und `"Reset Hourly".equals(event.getEntry().getOption())` → `resetSkillsHourly(skillKey)`.
|
|
||||||
- `getSkillStore()`, `getSkillKey()`, `getTimeFilter()` sind vorhandene Lombok-Getter; `skillsHudOverlay` ist injiziert (`EssentialsPlusPlugin.java:232`).
|
|
||||||
- Alternativ den `Consumer<MenuEntry>` der 4-arg-`addMenuEntry` nutzen; dann ist kein `OverlayMenuClicked`-Subscriber nötig (beides funktioniert). Empfehlung: Subscriber, damit die Aktion im Plugin kapselt bleibt.
|
|
||||||
|
|
||||||
### `ui/EssentialsPlusPanel.java` (Overview-Karte)
|
|
||||||
|
|
||||||
- In `menu(String id)` im `case SKILLS` (ca. Zeile 560) einen Eintrag ergänzen:
|
|
||||||
- `JMenuItem reset = new JMenuItem("Reset Hourly");`
|
|
||||||
- Action: Character über `SkillsPanel.resolve(plugin.getSkillStore(), null, plugin.getSkillKey())` auflösen; wenn nicht null → `plugin.resetSkillsHourly(h.key);` und danach `refreshSkills();`.
|
|
||||||
- **Kein** zweites Popup auf der Overview-Karte installieren: `CardDrag` (`ui/CardDrag.java`) konsumiert dort bereits Rechtsklick und öffnet `menu(id)` – sonst gäbe es zwei Menüs.
|
|
||||||
|
|
||||||
### `ui/SkillsPanel.java` (Skills-Tab)
|
|
||||||
|
|
||||||
- Nach `private final SkillsCard card = new SkillsCard(null);` (Zeile 61) im Konstruktor einen `MouseAdapter` auf `card` installieren:
|
|
||||||
- bei `SwingUtilities.isRightMouseButton(e)` (oder `e.isPopupTrigger()`) ein `JPopupMenu` mit `JMenuItem "Reset Hourly"` bauen und `menu.show(card, e.getX(), e.getY())` zeigen.
|
|
||||||
- Action: `plugin.resetSkillsHourly(shownKey);` danach `refresh();` (bzw. `refreshLater()`).
|
|
||||||
- Im Skills-Tab ist **keine** `CardDrag` installiert, daher kollidiert das Popup nicht.
|
|
||||||
|
|
||||||
## Verifikation
|
|
||||||
|
|
||||||
- `./gradlew test` (bestehende Tests + neuer Test unten) und `./gradlew build`.
|
|
||||||
- Neuer Test in `src/test/java/com/example/essentialsplus/SkillHistoryTest.java` (Muster: `record` per Reflection, package-private):
|
|
||||||
- Samples über ca. eine Stunde anlegen, dann `h.reset(TimeRange.CURRENT)` bzw. `h.reset(TimeRange.SESSION)`.
|
|
||||||
- Erwartung: `gained(filter)` für den betroffenen Range startet neu (Baseline = Sample bei `resetAt`); `gained(TimeRange.ALL)` bleibt unverändert; `1H`/`1D` bleiben unverändert.
|
|
||||||
- Manuell:
|
|
||||||
- Filter auf **Sess** oder **Now** stellen → Rechtsklick aufs XP-HUD → „Reset Hourly" → `XP gained` zählt ab 0 neu; Gesamt-XP (`All`) unverändert.
|
|
||||||
- Filter auf `1H`/`1D`/`All` stellen → „Reset Hourly" ändert die Anzeige nicht.
|
|
||||||
- Overview-Skills-Karte rechtsklicken → „Reset Hourly"; Skills-Tab-Karte rechtsklicken → „Reset Hourly".
|
|
||||||
- Nach Reset Datei `essentials-plus/skills/<key>.json` prüfen (`resetSessionAt`/`resetNowAt` gesetzt); Relog → Session wird ohnehin vom neuen Login übersteuert, `Now`-Marker bleibt gespeichert.
|
|
||||||
|
|
||||||
## Hinweise / Einschränkungen
|
|
||||||
|
|
||||||
- „Reset Hourly" wirkt bewusst **nur** auf `Session` und `Now`; bei `1H`/`1D`/`2W`/`3M`/`All` passiert nichts (`TimeRange`-Vergleich im `reset(...)`).
|
|
||||||
- Der Gesamtwert (`All`) bleibt in jedem Fall erhalten, da er aus der ersten Sample-Spur (`samples.get(0)`) berechnet wird und der Reset nur einen Start-Clamp setzt.
|
|
||||||
- Marker sind pro Charakter im `SkillHistory`-JSON persistiert; ein fehlendes Feld nach älterem Stand bedeutet 0 (nie zurückgesetzt) und ist damit abwärtskompatibel.
|
|
||||||
- `record(...)` bleibt unverändert: Es fügt weiterhin Samples hinzu; der Reset löscht keine Historie.
|
|
||||||
@@ -10,7 +10,7 @@ public interface ExampleConfig extends Config
|
|||||||
{
|
{
|
||||||
@ConfigSection(
|
@ConfigSection(
|
||||||
name = "General Settings",
|
name = "General Settings",
|
||||||
description = "Allgemeine Einstellungen für das Plugin",
|
description = "General plugin settings.",
|
||||||
position = 0
|
position = 0
|
||||||
)
|
)
|
||||||
String generalSection = "generalSection";
|
String generalSection = "generalSection";
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import net.runelite.client.util.ImageUtil;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@PluginDescriptor(
|
@PluginDescriptor(
|
||||||
name = "Bank Highlighter / Auto-Profile",
|
name = "Bank Highlighter / Auto-Profile",
|
||||||
description = "Aktiviert automatisch Profile, Item-Highlights, 3D-Objekte, Boden-Tiles und Pfade",
|
description = "Automatically manages profiles, item highlights, 3D objects, ground tiles, and paths",
|
||||||
tags = {"bank", "highlighter", "profile", "auto", "herblore", "tiles", "path", "agility"}
|
tags = {"bank", "highlighter", "profile", "auto", "herblore", "tiles", "path", "agility"}
|
||||||
)
|
)
|
||||||
public class ExamplePlugin extends Plugin implements ProfileManager.ProfileChangeListener
|
public class ExamplePlugin extends Plugin implements ProfileManager.ProfileChangeListener
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ import net.runelite.api.ItemContainer;
|
|||||||
import net.runelite.api.gameval.InterfaceID;
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
import net.runelite.api.gameval.InventoryID;
|
import net.runelite.api.gameval.InventoryID;
|
||||||
import net.runelite.api.gameval.ItemID;
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.widgets.ComponentID;
|
||||||
import net.runelite.api.widgets.Widget;
|
import net.runelite.api.widgets.Widget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,17 +37,21 @@ final class GrandExchangeTracker
|
|||||||
|
|
||||||
/** how long a visible coin loss may be matched to a new buy offer */
|
/** how long a visible coin loss may be matched to a new buy offer */
|
||||||
private static final long COIN_LOSS_WINDOW_MS = 15_000L;
|
private static final long COIN_LOSS_WINDOW_MS = 15_000L;
|
||||||
|
/** how long after a collection interface (GE, GE Collect, Bank) was open collections may be matched */
|
||||||
|
private static final long COLLECT_WINDOW_MS = 30_000L;
|
||||||
|
|
||||||
/** inventory + visible bank holdings (canonical ids) at the previous update */
|
/** inventory + visible bank holdings (canonical ids) at the previous update */
|
||||||
private Map<Integer, Long> previousHoldings;
|
private Map<Integer, Long> previousHoldings;
|
||||||
/** coins that recently left inventory / visible bank and were not matched to a buy offer yet */
|
/** coins that recently left inventory / visible bank and were not matched to a buy offer yet */
|
||||||
private long recentCoinLoss;
|
private long recentCoinLoss;
|
||||||
private long recentCoinLossAt;
|
private long recentCoinLossAt;
|
||||||
|
private long lastCollectInterfaceAt;
|
||||||
|
|
||||||
void reset()
|
void reset()
|
||||||
{
|
{
|
||||||
previousHoldings = null;
|
previousHoldings = null;
|
||||||
recentCoinLoss = 0;
|
recentCoinLoss = 0;
|
||||||
|
lastCollectInterfaceAt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +111,14 @@ final class GrandExchangeTracker
|
|||||||
}
|
}
|
||||||
boolean seeded = migrate;
|
boolean seeded = migrate;
|
||||||
|
|
||||||
boolean collecting = isOpen(client, InterfaceID.GE_OFFERS) || isOpen(client, InterfaceID.GE_COLLECT) || isOpen(client, InterfaceID.BANKMAIN);
|
if (isOpen(client, InterfaceID.GE_OFFERS)
|
||||||
|
|| isOpen(client, InterfaceID.GE_COLLECT)
|
||||||
|
|| isOpen(client, InterfaceID.BANKMAIN)
|
||||||
|
|| isOpen(client, InterfaceID.BANK_DEPOSITBOX))
|
||||||
|
{
|
||||||
|
lastCollectInterfaceAt = updateTime;
|
||||||
|
}
|
||||||
|
boolean collecting = (updateTime - lastCollectInterfaceAt <= COLLECT_WINDOW_MS);
|
||||||
|
|
||||||
for (int i = 0; i < SLOTS; i++)
|
for (int i = 0; i < SLOTS; i++)
|
||||||
{
|
{
|
||||||
@@ -155,6 +167,10 @@ final class GrandExchangeTracker
|
|||||||
fresh.placedAt = migrate ? 0 : now;
|
fresh.placedAt = migrate ? 0 : now;
|
||||||
fresh.done = migrate ? done : 0;
|
fresh.done = migrate ? done : 0;
|
||||||
fresh.trackedDone = fresh.done;
|
fresh.trackedDone = fresh.done;
|
||||||
|
int canItem = canonicalize.applyAsInt(fresh.item);
|
||||||
|
fresh.baselineHoldings = holdings.getOrDefault(canItem, 0L);
|
||||||
|
fresh.baselineCoins = holdings.getOrDefault(ItemID.COINS, 0L);
|
||||||
|
fresh.baselineInitialized = true;
|
||||||
if (migrate && acc.geSeedCollected != null)
|
if (migrate && acc.geSeedCollected != null)
|
||||||
{
|
{
|
||||||
// first run with this tracker: take over what is known to be collected already
|
// first run with this tracker: take over what is known to be collected already
|
||||||
@@ -185,6 +201,52 @@ final class GrandExchangeTracker
|
|||||||
s.collectedItems += consume(gains, item, Math.max(0, grossItems - s.collectedItems));
|
s.collectedItems += consume(gains, item, Math.max(0, grossItems - s.collectedItems));
|
||||||
s.collectedCoins += consume(gains, ItemID.COINS, Math.max(0, grossCoins - s.collectedCoins));
|
s.collectedCoins += consume(gains, ItemID.COINS, Math.max(0, grossCoins - s.collectedCoins));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In addition to real-time gains, enforce holdings floor so partial collections are NEVER double-counted
|
||||||
|
// even if an update tick was missed, the interface was closed before compute, or on client restarts.
|
||||||
|
if (s.buy)
|
||||||
|
{
|
||||||
|
long currentHoldings = holdings.getOrDefault(item, 0L);
|
||||||
|
if (!s.baselineInitialized)
|
||||||
|
{
|
||||||
|
s.baselineHoldings = Math.max(0L, currentHoldings - s.collectedItems);
|
||||||
|
s.baselineInitialized = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long netGain = Math.max(0L, currentHoldings - s.baselineHoldings);
|
||||||
|
if (netGain > 0)
|
||||||
|
{
|
||||||
|
long floor = Math.min(grossItems, netGain);
|
||||||
|
if (floor > s.collectedItems)
|
||||||
|
{
|
||||||
|
s.collectedItems = floor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long currentCoins = holdings.getOrDefault(ItemID.COINS, 0L);
|
||||||
|
if (!s.baselineInitialized)
|
||||||
|
{
|
||||||
|
s.baselineCoins = Math.max(0L, currentCoins - s.collectedCoins);
|
||||||
|
s.baselineInitialized = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long netGain = Math.max(0L, currentCoins - s.baselineCoins);
|
||||||
|
if (netGain > 0)
|
||||||
|
{
|
||||||
|
long floor = Math.min(grossCoins, netGain);
|
||||||
|
if (floor > s.collectedCoins)
|
||||||
|
{
|
||||||
|
s.collectedCoins = floor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
long collectItems = Math.max(0, grossItems - s.collectedItems);
|
long collectItems = Math.max(0, grossItems - s.collectedItems);
|
||||||
long collectCoins = Math.max(0, grossCoins - s.collectedCoins);
|
long collectCoins = Math.max(0, grossCoins - s.collectedCoins);
|
||||||
ItemContainer box = client.getItemContainer(COLLECT_CONTAINERS[i]);
|
ItemContainer box = client.getItemContainer(COLLECT_CONTAINERS[i]);
|
||||||
@@ -312,7 +374,21 @@ final class GrandExchangeTracker
|
|||||||
|
|
||||||
private static boolean isOpen(Client client, int group)
|
private static boolean isOpen(Client client, int group)
|
||||||
{
|
{
|
||||||
Widget w = client.getWidget(group, 0);
|
Widget w0 = client.getWidget(group, 0);
|
||||||
return w != null && !w.isHidden();
|
if (w0 != null && !w0.isHidden())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Widget w1 = client.getWidget(group, 1);
|
||||||
|
if (w1 != null && !w1.isHidden())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (group == InterfaceID.BANKMAIN)
|
||||||
|
{
|
||||||
|
Widget bank = client.getWidget(ComponentID.BANK_CONTAINER);
|
||||||
|
return bank != null && !bank.isHidden();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public class CurrencyHistory
|
|||||||
{
|
{
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
long start = filter.getRange() == TimeRange.CURRENT
|
long start = filter.isCurrent()
|
||||||
? TimeFilter.streakStart(s.size(), i -> s.get(i).t, i -> s.get(i).g > 0, now)
|
? TimeFilter.streakStart(s.size(), i -> s.get(i).t, i -> s.get(i).g > 0, now)
|
||||||
: filter.start();
|
: filter.start();
|
||||||
for (int i = s.size() - 1; i >= 0 && s.get(i).t >= start; i--)
|
for (int i = s.size() - 1; i >= 0 && s.get(i).t >= start; i--)
|
||||||
|
|||||||
@@ -49,16 +49,16 @@ public class AccountData
|
|||||||
public static class GeBuyLimitSlot
|
public static class GeBuyLimitSlot
|
||||||
{
|
{
|
||||||
public int itemId;
|
public int itemId;
|
||||||
public int price;
|
public long price;
|
||||||
public int total;
|
public int total;
|
||||||
public int quantity;
|
public int quantity;
|
||||||
public int spent;
|
public long spent;
|
||||||
|
|
||||||
public GeBuyLimitSlot()
|
public GeBuyLimitSlot()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public GeBuyLimitSlot(int itemId, int price, int total, int quantity, int spent)
|
public GeBuyLimitSlot(int itemId, long price, int total, int quantity, long spent)
|
||||||
{
|
{
|
||||||
this.itemId = itemId;
|
this.itemId = itemId;
|
||||||
this.price = price;
|
this.price = price;
|
||||||
@@ -302,6 +302,82 @@ public class AccountData
|
|||||||
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a sequence of consecutive snapshots starting at index 'start' with length 'count'
|
||||||
|
* represents a transient plateau spike (e.g. 4 to 25 points / up to 35 minutes) between
|
||||||
|
* before (start - 1) and after (start + count).
|
||||||
|
*/
|
||||||
|
public static boolean isPlateauSpike(List<Snapshot> list, int start, int count)
|
||||||
|
{
|
||||||
|
if (list == null || start + count >= list.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Snapshot after = list.get(start + count);
|
||||||
|
return isPlateauSpike(list, start, count, after.t, after.total());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPlateauSpike(List<Snapshot> list, int start, int count, long tAfter, long vAfter)
|
||||||
|
{
|
||||||
|
if (list == null || start <= 0 || count < 1 || start + count > list.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Snapshot before = list.get(start - 1);
|
||||||
|
Snapshot first = list.get(start);
|
||||||
|
Snapshot last = list.get(start + count - 1);
|
||||||
|
|
||||||
|
long legBefore = first.t - before.t;
|
||||||
|
long legAfter = tAfter - last.t;
|
||||||
|
if (legBefore <= 0 || legAfter <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long maxPlateauDuration = 35 * 60 * 1000L; // 35 minutes max
|
||||||
|
if (tAfter - before.t > maxPlateauDuration)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long vBefore = before.total();
|
||||||
|
|
||||||
|
boolean positive = (first.total() - vBefore) > 0;
|
||||||
|
long minDev = Long.MAX_VALUE;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
long val = list.get(start + i).total();
|
||||||
|
long d1 = val - vBefore;
|
||||||
|
long d2 = vAfter - val;
|
||||||
|
if (positive)
|
||||||
|
{
|
||||||
|
if (d1 <= 0 || d2 >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (d1 >= 0 || d2 <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
minDev = Math.min(minDev, Math.min(Math.abs(d1), Math.abs(d2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
long baseDelta = Math.abs(vAfter - vBefore);
|
||||||
|
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vBefore), Math.abs(vAfter)));
|
||||||
|
boolean significant = minDev >= MIN_SPIKE_ABS_GP || (double) minDev / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||||
|
if (!significant)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseDelta <= 0.35 * minDev
|
||||||
|
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters out momentary outlier spikes from the timeline (e.g. 1-minute spikes caused by
|
* Filters out momentary outlier spikes from the timeline (e.g. 1-minute spikes caused by
|
||||||
* transient container reloading, temporary GE duplication, or pricing glitches).
|
* transient container reloading, temporary GE duplication, or pricing glitches).
|
||||||
@@ -371,10 +447,88 @@ public class AccountData
|
|||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass 4: check multi-point plateau spikes (4 to 25 consecutive points)
|
||||||
|
for (int k = 25; k >= 4; k--)
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= list.size() - k - 1; i++)
|
||||||
|
{
|
||||||
|
if (isPlateauSpike(list, i, k))
|
||||||
|
{
|
||||||
|
for (int j = 0; j < k; j++)
|
||||||
|
{
|
||||||
|
list.remove(i);
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 5: check login step glitches (unverified cache before bank sync after a gap)
|
||||||
|
for (int i = 1; i < list.size() - 2; i++)
|
||||||
|
{
|
||||||
|
Snapshot b = list.get(i - 1);
|
||||||
|
Snapshot g = list.get(i);
|
||||||
|
Snapshot a1 = list.get(i + 1);
|
||||||
|
Snapshot a2 = list.get(i + 2);
|
||||||
|
if (isLoginStepGlitch(b.t, b.total(), g.t, g.total(), a1.t, a1.total(), a2.t, a2.total()))
|
||||||
|
{
|
||||||
|
list.remove(i);
|
||||||
|
changed = true;
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an isolated point immediately following a logout gap (>= 10 minutes) represents
|
||||||
|
* an unverified cache artifact from before the bank was synced in that session.
|
||||||
|
*/
|
||||||
|
public static boolean isLoginStepGlitch(long tBefore, long vBefore,
|
||||||
|
long tGlitch, long vGlitch,
|
||||||
|
long tAfter1, long vAfter1,
|
||||||
|
long tAfter2, long vAfter2)
|
||||||
|
{
|
||||||
|
if (tGlitch - tBefore < 10 * 60 * 1000L)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long glitchDuration = tAfter1 - tGlitch;
|
||||||
|
if (glitchDuration <= 0 || glitchDuration > 5 * 60 * 1000L)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tAfter2 - tAfter1 <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long drop = Math.abs(vAfter1 - vGlitch);
|
||||||
|
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vGlitch), Math.abs(vAfter1)));
|
||||||
|
boolean significant = drop >= MIN_SPIKE_ABS_GP || (double) drop / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||||
|
if (!significant)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sustained at after1 and after2
|
||||||
|
if (Math.abs(vAfter2 - vAfter1) > Math.max(500_000L, (long) (0.35 * drop)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glitch point closely matches the old before value from before the logout gap
|
||||||
|
if (Math.abs(vGlitch - vBefore) > Math.max(500_000L, (long) (0.35 * drop)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static final long FOUR_HOURS_MS = 4 * 3600 * 1000L;
|
public static final long FOUR_HOURS_MS = 4 * 3600 * 1000L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -459,4 +613,3 @@ public class AccountData
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ public class GeRow
|
|||||||
/** null = empty slot */
|
/** null = empty slot */
|
||||||
public final GeSlot offer;
|
public final GeSlot offer;
|
||||||
public final String name;
|
public final String name;
|
||||||
public final int marketPrice;
|
public final long marketPrice;
|
||||||
public final GeLimitInfo limitInfo;
|
public final GeLimitInfo limitInfo;
|
||||||
|
|
||||||
public GeRow(String character, int slot, GeSlot offer, String name, int marketPrice)
|
public GeRow(String character, int slot, GeSlot offer, String name, long marketPrice)
|
||||||
{
|
{
|
||||||
this(character, slot, offer, name, marketPrice, null);
|
this(character, slot, offer, name, marketPrice, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GeRow(String character, int slot, GeSlot offer, String name, int marketPrice, GeLimitInfo limitInfo)
|
public GeRow(String character, int slot, GeSlot offer, String name, long marketPrice, GeLimitInfo limitInfo)
|
||||||
{
|
{
|
||||||
this.character = character;
|
this.character = character;
|
||||||
this.slot = slot;
|
this.slot = slot;
|
||||||
|
|||||||
@@ -27,4 +27,9 @@ public class GeSlot
|
|||||||
public long trackedSince;
|
public long trackedSince;
|
||||||
public long trackedDone;
|
public long trackedDone;
|
||||||
public long lastTradeAt;
|
public long lastTradeAt;
|
||||||
|
|
||||||
|
/** Baseline holdings of the offer item across visible containers when offer was placed */
|
||||||
|
public long baselineHoldings;
|
||||||
|
public long baselineCoins;
|
||||||
|
public boolean baselineInitialized;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class ItemSnapshot
|
|||||||
public long t;
|
public long t;
|
||||||
public int[] ids;
|
public int[] ids;
|
||||||
public long[] q;
|
public long[] q;
|
||||||
public int[] p;
|
public long[] p;
|
||||||
/** pinned (session start) - kept by compaction */
|
/** pinned (session start) - kept by compaction */
|
||||||
public Boolean k;
|
public Boolean k;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ public class ItemSnapshot
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public ItemSnapshot(long t, int[] ids, long[] q, int[] p)
|
public ItemSnapshot(long t, int[] ids, long[] q, long[] p)
|
||||||
{
|
{
|
||||||
this.t = t;
|
this.t = t;
|
||||||
this.ids = ids;
|
this.ids = ids;
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackStyle;
|
||||||
|
import com.example.essentialsplus.helpers.combat.AttackType;
|
||||||
|
import com.example.essentialsplus.helpers.combat.CombatUtils;
|
||||||
|
import com.example.essentialsplus.helpers.combat.WeaponType;
|
||||||
|
import java.util.Locale;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.EquipmentInventorySlot;
|
||||||
|
import net.runelite.api.Item;
|
||||||
|
import net.runelite.api.ItemComposition;
|
||||||
|
import net.runelite.api.ItemContainer;
|
||||||
|
import net.runelite.api.gameval.InventoryID;
|
||||||
|
import net.runelite.api.gameval.VarPlayerID;
|
||||||
|
|
||||||
|
/** Resolves the active tile range for equipped weapons. */
|
||||||
|
public final class AttackDistance
|
||||||
|
{
|
||||||
|
private static final int MAX_RANGE = 10;
|
||||||
|
|
||||||
|
private AttackDistance()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RangeInfo getEquippedAttackRange(Client client)
|
||||||
|
{
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemContainer equipment = client.getItemContainer(InventoryID.WORN);
|
||||||
|
WeaponType weaponType = CombatUtils.getWeaponType(client);
|
||||||
|
AttackStyle style = getAttackStyle(client);
|
||||||
|
String styleName = style == null ? "Unknown" : style.getName();
|
||||||
|
Item weapon = equipment == null ? null : equipment.getItem(EquipmentInventorySlot.WEAPON.getSlotIdx());
|
||||||
|
if (weapon == null || weapon.getId() <= 0)
|
||||||
|
{
|
||||||
|
return weaponType == WeaponType.UNARMED ? new RangeInfo("Unarmed", 1, styleName) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemComposition composition = client.getItemDefinition(weapon.getId());
|
||||||
|
if (composition == null || composition.getName() == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String weaponName = composition.getName();
|
||||||
|
String normalizedName = weaponName.toLowerCase(Locale.ROOT);
|
||||||
|
AttackType attackType = getAttackType(client);
|
||||||
|
|
||||||
|
if ((weaponType == WeaponType.STAFF || weaponType == WeaponType.BLADED_STAFF)
|
||||||
|
&& (style == AttackStyle.CASTING || style == AttackStyle.DEFENSIVE_CASTING || attackType == AttackType.MAGIC))
|
||||||
|
{
|
||||||
|
// Standard combat spells have a 10-tile range, including autocast.
|
||||||
|
return new RangeInfo(weaponName, MAX_RANGE, styleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPoweredMagicWeapon(weaponType, normalizedName))
|
||||||
|
{
|
||||||
|
int baseRange = getPoweredMagicRange(normalizedName);
|
||||||
|
int range = hasLongRangeStyle(client, weaponType, normalizedName, style)
|
||||||
|
? Math.min(MAX_RANGE, baseRange + 2)
|
||||||
|
: baseRange;
|
||||||
|
return new RangeInfo(weaponName, range, styleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRangedWeapon(weaponType, normalizedName) || attackType == AttackType.RANGED)
|
||||||
|
{
|
||||||
|
int baseRange = getRangedBaseRange(normalizedName, weaponType);
|
||||||
|
int range = hasLongRangeStyle(client, weaponType, normalizedName, style)
|
||||||
|
? Math.min(MAX_RANGE, baseRange + 2)
|
||||||
|
: baseRange;
|
||||||
|
return new RangeInfo(weaponName, range, styleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attackType != null && attackType != AttackType.NONE)
|
||||||
|
{
|
||||||
|
int range = normalizedName.contains("halberd") || weaponType == WeaponType.POLEARM_CRYSTAL_HALBERD ? 2 : 1;
|
||||||
|
return new RangeInfo(weaponName, range, styleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRangedWeapon(WeaponType weaponType, String name)
|
||||||
|
{
|
||||||
|
return weaponType == WeaponType.BOW
|
||||||
|
|| weaponType == WeaponType.HEAVY_RANGED
|
||||||
|
|| weaponType == WeaponType.THROWN
|
||||||
|
|| weaponType == WeaponType.CHINCHOMPA
|
||||||
|
|| weaponType == WeaponType.SALAMANDER
|
||||||
|
|| name.contains("bow")
|
||||||
|
|| name.contains("ballista")
|
||||||
|
|| name.contains("seercull")
|
||||||
|
|| name.contains("dart")
|
||||||
|
|| name.contains("knife")
|
||||||
|
|| name.contains("throwing axe")
|
||||||
|
|| name.contains("chinchompa")
|
||||||
|
|| name.contains("toktz-xil-ul")
|
||||||
|
|| name.contains("blowpipe")
|
||||||
|
|| name.contains("hunter") && name.contains("spear")
|
||||||
|
|| name.contains("eclipse atlatl")
|
||||||
|
|| name.contains("tonalztics")
|
||||||
|
|| name.contains("morrigan") && (name.contains("javelin") || name.contains("axe"))
|
||||||
|
|| name.contains("holy water")
|
||||||
|
|| name.contains("mud pie");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getRangedBaseRange(String name, WeaponType weaponType)
|
||||||
|
{
|
||||||
|
if (weaponType == WeaponType.SALAMANDER)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (name.contains("dart"))
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
if (name.contains("knife") || name.contains("throwing axe") || name.contains("holy water"))
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (name.contains("phoenix crossbow") || name.contains("comp ogre bow")
|
||||||
|
|| name.contains("blowpipe") || name.contains("hunter") && name.contains("spear"))
|
||||||
|
{
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
if (name.contains("morrigan") && name.contains("javelin"))
|
||||||
|
{
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
if (name.contains("dorgeshuun crossbow") || name.contains("cursed goblin bow")
|
||||||
|
|| name.contains("venator bow") || name.contains("eclipse atlatl")
|
||||||
|
|| name.contains("tonalztics") || name.contains("mud pie"))
|
||||||
|
{
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
if (weaponType == WeaponType.CHINCHOMPA || name.contains("chinchompa"))
|
||||||
|
{
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
if (name.contains("toktz-xil-ul"))
|
||||||
|
{
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
if (name.contains("karil")
|
||||||
|
|| (name.contains("hunter") && name.contains("crossbow") && !name.contains("dragon"))
|
||||||
|
|| name.contains("armadyl crossbow")
|
||||||
|
|| name.contains("zaryte crossbow"))
|
||||||
|
{
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
if (name.contains("ballista")
|
||||||
|
|| name.contains("3rd age bow")
|
||||||
|
|| name.contains("third-age bow")
|
||||||
|
|| name.contains("craw")
|
||||||
|
|| name.contains("webweaver bow"))
|
||||||
|
{
|
||||||
|
return 9;
|
||||||
|
}
|
||||||
|
if (name.contains("seercull"))
|
||||||
|
{
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
if (name.equals("crossbow"))
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
if (name.contains("longbow")
|
||||||
|
|| name.contains("comp bow")
|
||||||
|
|| name.contains("composite bow")
|
||||||
|
|| name.contains("ogre bow")
|
||||||
|
|| name.contains("crystal bow")
|
||||||
|
|| name.contains("corrupted bow")
|
||||||
|
|| name.contains("dark bow")
|
||||||
|
|| name.contains("scorching bow")
|
||||||
|
|| name.contains("faerdhinen")
|
||||||
|
|| name.contains("twisted bow"))
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
if (weaponType == WeaponType.THROWN)
|
||||||
|
{
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return weaponType == WeaponType.CHINCHOMPA ? 9 : 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasLongRangeStyle(Client client, WeaponType weaponType, String name, AttackStyle style)
|
||||||
|
{
|
||||||
|
if (style == AttackStyle.LONGRANGE)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chinchompas call their extended range style "Long fuse" rather than "Longrange".
|
||||||
|
return weaponType != WeaponType.SALAMANDER
|
||||||
|
&& client.getVarpValue(VarPlayerID.COM_MODE) == 2
|
||||||
|
&& (isRangedWeapon(weaponType, name) || isPoweredMagicWeapon(weaponType, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getPoweredMagicRange(String name)
|
||||||
|
{
|
||||||
|
if (name.contains("eye of ayak"))
|
||||||
|
{
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
if (name.contains("tumeken's heka") || name.contains("tumeken’s heka"))
|
||||||
|
{
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
if (name.contains("bone staff") || name.contains("tumeken's shadow") || name.contains("tumeken’s shadow"))
|
||||||
|
{
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
if (name.contains("crystal staff") || name.contains("corrupted staff"))
|
||||||
|
{
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPoweredMagicWeapon(WeaponType weaponType, String name)
|
||||||
|
{
|
||||||
|
return weaponType == WeaponType.POWERED_STAFF
|
||||||
|
|| weaponType == WeaponType.POWERED_WAND
|
||||||
|
|| name.contains("trident")
|
||||||
|
|| name.contains("crystal staff")
|
||||||
|
|| name.contains("corrupted staff")
|
||||||
|
|| name.contains("starter staff")
|
||||||
|
|| name.contains("tumeken's heka")
|
||||||
|
|| name.contains("tumeken’s heka");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttackStyle getAttackStyle(Client client)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return CombatUtils.getAttackStyle(client);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttackType getAttackType(Client client)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return CombatUtils.getAttackType(client);
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class RangeInfo
|
||||||
|
{
|
||||||
|
private final String weaponName;
|
||||||
|
private final int tiles;
|
||||||
|
private final String styleName;
|
||||||
|
|
||||||
|
private RangeInfo(String weaponName, int tiles, String styleName)
|
||||||
|
{
|
||||||
|
this.weaponName = weaponName;
|
||||||
|
this.tiles = tiles;
|
||||||
|
this.styleName = styleName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWeaponName()
|
||||||
|
{
|
||||||
|
return weaponName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTiles()
|
||||||
|
{
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStyleName()
|
||||||
|
{
|
||||||
|
return styleName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import com.example.essentialsplus.EssentialsPlusPlugin;
|
||||||
|
import java.awt.BasicStroke;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Polygon;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import net.runelite.api.Actor;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.NPC;
|
||||||
|
import net.runelite.api.Perspective;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.Scene;
|
||||||
|
import net.runelite.api.Tile;
|
||||||
|
import net.runelite.api.WorldView;
|
||||||
|
import net.runelite.api.coords.LocalPoint;
|
||||||
|
import net.runelite.api.coords.WorldArea;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
import net.runelite.client.ui.overlay.Overlay;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayLayer;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayPosition;
|
||||||
|
|
||||||
|
/** Outlines the attack range of the equipped weapon. */
|
||||||
|
public class AttackDistanceOverlay extends Overlay
|
||||||
|
{
|
||||||
|
private static final Color RANGE_BORDER = new Color(255, 190, 66, 180);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final EssentialsPlusPlugin plugin;
|
||||||
|
private long lastCombatNanos;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AttackDistanceOverlay(Client client, EssentialsPlusConfig config, EssentialsPlusPlugin plugin)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
this.plugin = plugin;
|
||||||
|
setPosition(OverlayPosition.DYNAMIC);
|
||||||
|
setLayer(OverlayLayer.ABOVE_SCENE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension render(Graphics2D graphics)
|
||||||
|
{
|
||||||
|
if (!config.attackDistance() || client.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = client.getLocalPlayer();
|
||||||
|
if (player == null || player.getWorldArea() == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (plugin.getHelperValue("attackDistanceHideOutOfCombat", Boolean.class, false) && !shouldShowAfterCombat(player))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AttackDistance.RangeInfo range = AttackDistance.getEquippedAttackRange(client);
|
||||||
|
if (range == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldView worldView = player.getWorldView();
|
||||||
|
Scene scene = worldView == null ? null : worldView.getScene();
|
||||||
|
Tile[][][] tiles = scene == null ? null : scene.getTiles();
|
||||||
|
int plane = worldView == null ? -1 : worldView.getPlane();
|
||||||
|
if (tiles == null || plane < 0 || plane >= tiles.length || tiles[plane] == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
Map<Edge, Integer> edges = new HashMap<>();
|
||||||
|
WorldArea playerArea = player.getWorldArea();
|
||||||
|
for (Tile[] row : tiles[plane])
|
||||||
|
{
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Tile tile : row)
|
||||||
|
{
|
||||||
|
if (tile == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldPoint location = tile.getWorldLocation();
|
||||||
|
if (location == null || location.getPlane() != plane || playerArea.distanceTo(location) > range.getTiles())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalPoint localPoint = tile.getLocalLocation();
|
||||||
|
if (localPoint == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Polygon polygon = Perspective.getCanvasTilePoly(client, localPoint);
|
||||||
|
if (polygon == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEdges(edges, polygon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics.setColor(RANGE_BORDER);
|
||||||
|
graphics.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
for (Map.Entry<Edge, Integer> entry : edges.entrySet())
|
||||||
|
{
|
||||||
|
if (entry.getValue() == 1)
|
||||||
|
{
|
||||||
|
Edge edge = entry.getKey();
|
||||||
|
graphics.drawLine(edge.x1, edge.y1, edge.x2, edge.y2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldShowAfterCombat(Player player)
|
||||||
|
{
|
||||||
|
long now = System.nanoTime();
|
||||||
|
if (isInCombat(player))
|
||||||
|
{
|
||||||
|
lastCombatNanos = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastCombatNanos != 0 && now - lastCombatNanos < 30_000_000_000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInCombat(Player player)
|
||||||
|
{
|
||||||
|
Actor target = player.getInteracting();
|
||||||
|
if (target != null && !target.isDead())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldView worldView = player.getWorldView();
|
||||||
|
if (worldView == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NPC npc : worldView.npcs())
|
||||||
|
{
|
||||||
|
if (npc != null && !npc.isDead() && npc.getInteracting() == player)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Player other : worldView.players())
|
||||||
|
{
|
||||||
|
if (other != null && other != player && !other.isDead() && other.getInteracting() == player)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addEdges(Map<Edge, Integer> edges, Polygon polygon)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < polygon.npoints; i++)
|
||||||
|
{
|
||||||
|
int next = (i + 1) % polygon.npoints;
|
||||||
|
Edge edge = new Edge(polygon.xpoints[i], polygon.ypoints[i], polygon.xpoints[next], polygon.ypoints[next]);
|
||||||
|
edges.merge(edge, 1, Integer::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Edge
|
||||||
|
{
|
||||||
|
private final int x1;
|
||||||
|
private final int y1;
|
||||||
|
private final int x2;
|
||||||
|
private final int y2;
|
||||||
|
|
||||||
|
private Edge(int x1, int y1, int x2, int y2)
|
||||||
|
{
|
||||||
|
if (x1 < x2 || (x1 == x2 && y1 <= y2))
|
||||||
|
{
|
||||||
|
this.x1 = x1;
|
||||||
|
this.y1 = y1;
|
||||||
|
this.x2 = x2;
|
||||||
|
this.y2 = y2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.x1 = x2;
|
||||||
|
this.y1 = y2;
|
||||||
|
this.x2 = x1;
|
||||||
|
this.y2 = y1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other)
|
||||||
|
{
|
||||||
|
if (this == other)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(other instanceof Edge))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Edge edge = (Edge) other;
|
||||||
|
return x1 == edge.x1 && y1 == edge.y1 && x2 == edge.x2 && y2 == edge.y2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hash(x1, y1, x2, y2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import java.awt.Toolkit;
|
|||||||
import java.awt.datatransfer.StringSelection;
|
import java.awt.datatransfer.StringSelection;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -17,15 +18,18 @@ import net.runelite.api.ChatMessageType;
|
|||||||
import net.runelite.api.Client;
|
import net.runelite.api.Client;
|
||||||
import net.runelite.api.GameState;
|
import net.runelite.api.GameState;
|
||||||
import net.runelite.api.MenuAction;
|
import net.runelite.api.MenuAction;
|
||||||
|
import net.runelite.api.MenuEntry;
|
||||||
import net.runelite.api.MessageNode;
|
import net.runelite.api.MessageNode;
|
||||||
import net.runelite.api.Player;
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.Point;
|
||||||
|
import net.runelite.api.ScriptID;
|
||||||
import net.runelite.api.events.ChatMessage;
|
import net.runelite.api.events.ChatMessage;
|
||||||
import net.runelite.api.events.GameStateChanged;
|
import net.runelite.api.events.GameStateChanged;
|
||||||
import net.runelite.api.events.GameTick;
|
import net.runelite.api.events.GameTick;
|
||||||
import net.runelite.api.events.MenuEntryAdded;
|
import net.runelite.api.events.MenuEntryAdded;
|
||||||
|
import net.runelite.api.events.MenuOpened;
|
||||||
import net.runelite.api.events.ScriptCallbackEvent;
|
import net.runelite.api.events.ScriptCallbackEvent;
|
||||||
import net.runelite.api.gameval.InterfaceID;
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
import net.runelite.api.widgets.ComponentID;
|
|
||||||
import net.runelite.api.widgets.Widget;
|
import net.runelite.api.widgets.Widget;
|
||||||
import net.runelite.api.widgets.WidgetUtil;
|
import net.runelite.api.widgets.WidgetUtil;
|
||||||
import net.runelite.client.callback.ClientThread;
|
import net.runelite.client.callback.ClientThread;
|
||||||
@@ -183,10 +187,17 @@ public class ChatHelper
|
|||||||
|
|
||||||
if (removedAny)
|
if (removedAny)
|
||||||
{
|
{
|
||||||
client.runScript(83);
|
refreshChat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshChat()
|
||||||
|
{
|
||||||
|
// Invalidate the cached chat before rebuilding the chatbox and split PMs.
|
||||||
|
client.refreshChat();
|
||||||
|
client.runScript(ScriptID.SPLITPM_CHANGED);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldClearType(ChatMessageType type)
|
private boolean shouldClearType(ChatMessageType type)
|
||||||
{
|
{
|
||||||
if (type == null)
|
if (type == null)
|
||||||
@@ -204,13 +215,22 @@ public class ChatHelper
|
|||||||
case SPAM:
|
case SPAM:
|
||||||
case BROADCAST:
|
case BROADCAST:
|
||||||
case CONSOLE:
|
case CONSOLE:
|
||||||
|
case WELCOME:
|
||||||
|
case PLAYERRELATED:
|
||||||
|
case TENSECTIMEOUT:
|
||||||
|
case SNAPSHOTFEEDBACK:
|
||||||
|
case FRIENDNOTIFICATION:
|
||||||
|
case IGNORENOTIFICATION:
|
||||||
return config.chatClearGame();
|
return config.chatClearGame();
|
||||||
|
|
||||||
case PUBLICCHAT:
|
case PUBLICCHAT:
|
||||||
case MODCHAT:
|
case MODCHAT:
|
||||||
case AUTOTYPER:
|
case AUTOTYPER:
|
||||||
|
case MODAUTOTYPER:
|
||||||
case TRADEREQ:
|
case TRADEREQ:
|
||||||
case TRADE:
|
case TRADE:
|
||||||
|
case TRADE_SENT:
|
||||||
|
case CHALREQ_TRADE:
|
||||||
return config.chatClearPublic();
|
return config.chatClearPublic();
|
||||||
|
|
||||||
case PRIVATECHAT:
|
case PRIVATECHAT:
|
||||||
@@ -222,6 +242,8 @@ public class ChatHelper
|
|||||||
case CLAN_GUEST_CHAT:
|
case CLAN_GUEST_CHAT:
|
||||||
case FRIENDSCHAT:
|
case FRIENDSCHAT:
|
||||||
case CLAN_MESSAGE:
|
case CLAN_MESSAGE:
|
||||||
|
case CLAN_GUEST_MESSAGE:
|
||||||
|
case FRIENDSCHATNOTIFICATION:
|
||||||
case CLAN_GIM_CHAT:
|
case CLAN_GIM_CHAT:
|
||||||
case CLAN_GIM_MESSAGE:
|
case CLAN_GIM_MESSAGE:
|
||||||
return config.chatClearClan();
|
return config.chatClearClan();
|
||||||
@@ -349,7 +371,16 @@ public class ChatHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Highlight own name
|
// 3. Recolor clan chat text without changing public, private, or game messages.
|
||||||
|
if (isClanChatType(ChatMessageType.of(messageTypeVal)))
|
||||||
|
{
|
||||||
|
Color clanColor = config.chatClanMessageColor();
|
||||||
|
String hex = ColorUtil.colorToHexCode(clanColor != null ? clanColor : new Color(0x66FFFF));
|
||||||
|
message = "<col=" + hex + ">" + message + "</col>";
|
||||||
|
objectStack[objectStackSize - 1] = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Highlight own name
|
||||||
if (config.chatHighlightName() && isPlayerSpeechType(messageTypeVal))
|
if (config.chatHighlightName() && isPlayerSpeechType(messageTypeVal))
|
||||||
{
|
{
|
||||||
String highlighted = highlightPlayerName(message);
|
String highlighted = highlightPlayerName(message);
|
||||||
@@ -360,6 +391,13 @@ public class ChatHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isClanChatType(ChatMessageType type)
|
||||||
|
{
|
||||||
|
return type == ChatMessageType.CLAN_CHAT
|
||||||
|
|| type == ChatMessageType.CLAN_GUEST_CHAT
|
||||||
|
|| type == ChatMessageType.CLAN_GIM_CHAT;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isCollapsible(ChatMessageType type)
|
private boolean isCollapsible(ChatMessageType type)
|
||||||
{
|
{
|
||||||
if (type == null)
|
if (type == null)
|
||||||
@@ -495,18 +533,17 @@ public class ChatHelper
|
|||||||
if (config.chatCopyToClipboard() && "Report".equals(event.getOption()))
|
if (config.chatCopyToClipboard() && "Report".equals(event.getOption()))
|
||||||
{
|
{
|
||||||
Widget lineWidget = client.getWidget(interfaceId, childId);
|
Widget lineWidget = client.getWidget(interfaceId, childId);
|
||||||
if (lineWidget != null && lineWidget.getParent() != null && lineWidget.getParent().getId() == ComponentID.CHATBOX_MESSAGE_LINES)
|
if (lineWidget != null && lineWidget.getParent() != null && lineWidget.getParent().getId() == InterfaceID.Chatbox.SCROLLAREA)
|
||||||
{
|
{
|
||||||
int lineIndex = (childId - WidgetUtil.componentToId(ComponentID.CHATBOX_FRAME)) * 4 + 1;
|
int lineIndex = (childId - WidgetUtil.componentToId(InterfaceID.Chatbox.LINE0)) * 4 + 1;
|
||||||
|
if (lineIndex < 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
Widget textWidget = lineWidget.getParent().getChild(lineIndex);
|
Widget textWidget = lineWidget.getParent().getChild(lineIndex);
|
||||||
if (textWidget != null && textWidget.getText() != null)
|
if (textWidget != null && textWidget.getText() != null)
|
||||||
{
|
{
|
||||||
String fullText = textWidget.getText();
|
addCopyEntry(textWidget.getText(), event.getTarget());
|
||||||
client.createMenuEntry(1)
|
|
||||||
.setOption("Copy message")
|
|
||||||
.setTarget(event.getTarget())
|
|
||||||
.setType(MenuAction.RUNELITE)
|
|
||||||
.onClick(e -> copyToClipboard(fullText));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,7 +559,73 @@ public class ChatHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void copyToClipboard(String text)
|
/** Game messages and transparent chat text do not necessarily have a Report action. */
|
||||||
|
@Subscribe(priority = -1)
|
||||||
|
public void onMenuOpened(MenuOpened event)
|
||||||
|
{
|
||||||
|
if (!config.chatHelper() || !config.chatCopyToClipboard())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MenuEntry[] entries = client.getMenu().getMenuEntries();
|
||||||
|
if (Arrays.stream(entries).anyMatch(e -> "Copy message".equals(e.getOption())))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Do not intercept another interface or an overlay covering the chat.
|
||||||
|
if (Arrays.stream(entries).anyMatch(e -> e.getWidget() != null
|
||||||
|
&& WidgetUtil.componentToInterface(e.getWidget().getId()) != InterfaceID.CHATBOX))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Widget scroll = client.getWidget(InterfaceID.Chatbox.SCROLLAREA);
|
||||||
|
Point mouse = client.getMouseCanvasPosition();
|
||||||
|
if (scroll == null || scroll.isHidden() || mouse == null
|
||||||
|
|| !scroll.getBounds().contains(mouse.getX(), mouse.getY()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Widget[] children = scroll.getDynamicChildren();
|
||||||
|
if (children == null || children.length == 0)
|
||||||
|
{
|
||||||
|
children = scroll.getChildren();
|
||||||
|
}
|
||||||
|
if (children == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 1; i < children.length; i += 4)
|
||||||
|
{
|
||||||
|
Widget message = children[i];
|
||||||
|
if (message != null && !message.isHidden() && message.getText() != null
|
||||||
|
&& !Text.removeTags(message.getText()).trim().isEmpty()
|
||||||
|
&& message.getBounds().contains(mouse.getX(), mouse.getY()))
|
||||||
|
{
|
||||||
|
// A world-only menu here is click-through from transparent chat.
|
||||||
|
boolean hasChatAction = Arrays.stream(entries).anyMatch(e ->
|
||||||
|
WidgetUtil.componentToInterface(e.getParam1()) == InterfaceID.CHATBOX);
|
||||||
|
if (!hasChatAction)
|
||||||
|
{
|
||||||
|
client.getMenu().setMenuEntries(Arrays.stream(entries)
|
||||||
|
.filter(e -> e.getType() == MenuAction.CANCEL).toArray(MenuEntry[]::new));
|
||||||
|
}
|
||||||
|
addCopyEntry(message.getText(), "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCopyEntry(String text, String target)
|
||||||
|
{
|
||||||
|
// Capture the displayed text now: new messages can arrive while the menu is open.
|
||||||
|
client.createMenuEntry(Math.min(1, client.getMenu().getMenuEntries().length))
|
||||||
|
.setOption("Copy message")
|
||||||
|
.setTarget(target)
|
||||||
|
.setType(MenuAction.RUNELITE)
|
||||||
|
.onClick(e -> copyToClipboard(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
void copyToClipboard(String text)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -572,7 +675,8 @@ public class ChatHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.runScript(83);
|
reset();
|
||||||
|
refreshChat();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import com.example.essentialsplus.helpers.combat.Attacking;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.Skill;
|
||||||
|
import net.runelite.api.SoundEffectVolume;
|
||||||
|
import net.runelite.api.coords.LocalPoint;
|
||||||
|
import net.runelite.api.coords.WorldPoint;
|
||||||
|
import net.runelite.api.events.AnimationChanged;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.ItemContainerChanged;
|
||||||
|
import net.runelite.api.events.MenuOptionClicked;
|
||||||
|
import net.runelite.api.events.WidgetClosed;
|
||||||
|
import net.runelite.api.events.WidgetDrag;
|
||||||
|
import net.runelite.api.events.WidgetLoaded;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.gameval.InventoryID;
|
||||||
|
import net.runelite.client.eventbus.Subscribe;
|
||||||
|
import net.runelite.client.game.NPCManager;
|
||||||
|
import net.runelite.client.ui.overlay.Overlay;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayLayer;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayPosition;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class IdleNotifier extends Overlay
|
||||||
|
{
|
||||||
|
private static final int FLASH_BORDER_SIZE = 28;
|
||||||
|
private static final Color FLASH_COLOR = new Color(255, 152, 0, 135);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final NPCManager npcManager;
|
||||||
|
|
||||||
|
private long actionStartedAt;
|
||||||
|
private long lastActionAt;
|
||||||
|
private volatile long flashStartedAt;
|
||||||
|
private long nextRepeatAt;
|
||||||
|
private long repeatUntilAt;
|
||||||
|
private WorldPoint lastPlayerLocation;
|
||||||
|
private LocalPoint lastPlayerLocalLocation;
|
||||||
|
private final int[] lastSkillExperience = new int[Skill.values().length];
|
||||||
|
private boolean experienceInitialized;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public IdleNotifier(Client client, EssentialsPlusConfig config, NPCManager npcManager)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
this.npcManager = npcManager;
|
||||||
|
setPosition(OverlayPosition.DYNAMIC);
|
||||||
|
setLayer(OverlayLayer.ALWAYS_ON_TOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onAnimationChanged(AnimationChanged event)
|
||||||
|
{
|
||||||
|
Player player = client.getLocalPlayer();
|
||||||
|
if (isEnabled() && player != null && event.getActor() == player && player.getAnimation() != -1)
|
||||||
|
{
|
||||||
|
recordAction(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onMenuOptionClicked(MenuOptionClicked event)
|
||||||
|
{
|
||||||
|
recordUserInteraction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onWidgetDrag(WidgetDrag event)
|
||||||
|
{
|
||||||
|
recordUserInteraction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onItemContainerChanged(ItemContainerChanged event)
|
||||||
|
{
|
||||||
|
int containerId = event.getContainerId();
|
||||||
|
if (containerId == InventoryID.INV || containerId == InventoryID.WORN || containerId == InventoryID.BANK)
|
||||||
|
{
|
||||||
|
recordUserInteraction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onWidgetLoaded(WidgetLoaded event)
|
||||||
|
{
|
||||||
|
if (event.getGroupId() == InterfaceID.BANKMAIN)
|
||||||
|
{
|
||||||
|
recordUserInteraction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onWidgetClosed(WidgetClosed event)
|
||||||
|
{
|
||||||
|
if (event.getGroupId() == InterfaceID.BANKMAIN)
|
||||||
|
{
|
||||||
|
recordUserInteraction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameTick(GameTick event)
|
||||||
|
{
|
||||||
|
if (!isEnabled() || client.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = client.getLocalPlayer();
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
WorldPoint location = player.getWorldLocation();
|
||||||
|
LocalPoint localLocation = player.getLocalLocation();
|
||||||
|
LocalPoint destination = client.getLocalDestinationLocation();
|
||||||
|
boolean moved = (location != null && lastPlayerLocation != null && !location.equals(lastPlayerLocation))
|
||||||
|
|| (localLocation != null && lastPlayerLocalLocation != null && !localLocation.equals(lastPlayerLocalLocation))
|
||||||
|
|| (localLocation != null && destination != null && !localLocation.equals(destination))
|
||||||
|
|| hasMovementPose(player);
|
||||||
|
lastPlayerLocation = location;
|
||||||
|
lastPlayerLocalLocation = localLocation;
|
||||||
|
boolean gainedSkillXp = hasSkillExperienceChanged();
|
||||||
|
|
||||||
|
boolean acting = player.getAnimation() != -1
|
||||||
|
|| Attacking.isPlayerAttacking(client, npcManager)
|
||||||
|
|| gainedSkillXp;
|
||||||
|
if (moved)
|
||||||
|
{
|
||||||
|
cancelIdleCycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acting)
|
||||||
|
{
|
||||||
|
recordAction(now);
|
||||||
|
}
|
||||||
|
else if (!moved)
|
||||||
|
{
|
||||||
|
finishIfIdle(now);
|
||||||
|
repeatIfIdle(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEnabled()
|
||||||
|
{
|
||||||
|
return config.idleNotifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasMovementPose(Player player)
|
||||||
|
{
|
||||||
|
int pose = player.getPoseAnimation();
|
||||||
|
return pose != -1 && (matchesMovementAnimation(pose, player.getWalkAnimation())
|
||||||
|
|| matchesMovementAnimation(pose, player.getRunAnimation())
|
||||||
|
|| matchesMovementAnimation(pose, player.getWalkRotateLeft())
|
||||||
|
|| matchesMovementAnimation(pose, player.getWalkRotateRight())
|
||||||
|
|| matchesMovementAnimation(pose, player.getWalkRotate180()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesMovementAnimation(int pose, int movementAnimation)
|
||||||
|
{
|
||||||
|
return movementAnimation != -1 && pose == movementAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasSkillExperienceChanged()
|
||||||
|
{
|
||||||
|
Skill[] skills = Skill.values();
|
||||||
|
if (!experienceInitialized)
|
||||||
|
{
|
||||||
|
for (Skill skill : skills)
|
||||||
|
{
|
||||||
|
if (skill != Skill.OVERALL)
|
||||||
|
{
|
||||||
|
lastSkillExperience[skill.ordinal()] = client.getSkillExperience(skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
experienceInitialized = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean gainedXp = false;
|
||||||
|
for (Skill skill : skills)
|
||||||
|
{
|
||||||
|
if (skill == Skill.OVERALL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int experience = client.getSkillExperience(skill);
|
||||||
|
if (experience > lastSkillExperience[skill.ordinal()])
|
||||||
|
{
|
||||||
|
gainedXp = true;
|
||||||
|
}
|
||||||
|
lastSkillExperience[skill.ordinal()] = experience;
|
||||||
|
}
|
||||||
|
return gainedXp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordAction(long now)
|
||||||
|
{
|
||||||
|
cancelRepeats();
|
||||||
|
flashStartedAt = 0;
|
||||||
|
if (actionStartedAt == 0)
|
||||||
|
{
|
||||||
|
actionStartedAt = now;
|
||||||
|
}
|
||||||
|
lastActionAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordUserInteraction()
|
||||||
|
{
|
||||||
|
if (isEnabled() && client.getGameState() == GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
recordAction(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelIdleCycle()
|
||||||
|
{
|
||||||
|
actionStartedAt = 0;
|
||||||
|
lastActionAt = 0;
|
||||||
|
flashStartedAt = 0;
|
||||||
|
cancelRepeats();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishIfIdle(long now)
|
||||||
|
{
|
||||||
|
if (lastActionAt == 0 || now - lastActionAt < config.idleNotifierIdleSeconds() * 1000L)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastActionAt - actionStartedAt >= config.idleNotifierMinActionSeconds() * 1000L)
|
||||||
|
{
|
||||||
|
notifyIdle(now);
|
||||||
|
if (config.idleNotifierRepeat())
|
||||||
|
{
|
||||||
|
long interval = config.idleNotifierRepeatIntervalSeconds() * 1000L;
|
||||||
|
repeatUntilAt = now + config.idleNotifierRepeatDurationSeconds() * 1000L;
|
||||||
|
nextRepeatAt = now + interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actionStartedAt = 0;
|
||||||
|
lastActionAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void repeatIfIdle(long now)
|
||||||
|
{
|
||||||
|
if (!config.idleNotifierRepeat())
|
||||||
|
{
|
||||||
|
cancelRepeats();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextRepeatAt == 0 || nextRepeatAt > repeatUntilAt)
|
||||||
|
{
|
||||||
|
cancelRepeats();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now >= nextRepeatAt)
|
||||||
|
{
|
||||||
|
notifyIdle(now);
|
||||||
|
nextRepeatAt += config.idleNotifierRepeatIntervalSeconds() * 1000L;
|
||||||
|
if (nextRepeatAt > repeatUntilAt)
|
||||||
|
{
|
||||||
|
cancelRepeats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyIdle(long now)
|
||||||
|
{
|
||||||
|
flashStartedAt = now;
|
||||||
|
if (config.idleNotifierSound())
|
||||||
|
{
|
||||||
|
playConfiguredSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelRepeats()
|
||||||
|
{
|
||||||
|
nextRepeatAt = 0;
|
||||||
|
repeatUntilAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset()
|
||||||
|
{
|
||||||
|
actionStartedAt = 0;
|
||||||
|
lastActionAt = 0;
|
||||||
|
lastPlayerLocation = null;
|
||||||
|
lastPlayerLocalLocation = null;
|
||||||
|
flashStartedAt = 0;
|
||||||
|
cancelRepeats();
|
||||||
|
experienceInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playConfiguredSound()
|
||||||
|
{
|
||||||
|
if (client.getPreferences() == null || client.getPreferences().getSoundEffectVolume() == SoundEffectVolume.MUTED)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.playSoundEffect(config.idleNotifierSoundId(), config.idleNotifierSoundVolume());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension render(Graphics2D graphics)
|
||||||
|
{
|
||||||
|
if (!isEnabled() || flashStartedAt == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
long elapsed = System.currentTimeMillis() - flashStartedAt;
|
||||||
|
if (elapsed >= config.idleNotifierFlashDurationMs())
|
||||||
|
{
|
||||||
|
flashStartedAt = 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ((elapsed / config.idleNotifierBlinkIntervalMs()) % 2 != 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = client.getCanvasWidth();
|
||||||
|
int height = client.getCanvasHeight();
|
||||||
|
graphics.setColor(FLASH_COLOR);
|
||||||
|
graphics.fillRect(0, 0, width, FLASH_BORDER_SIZE);
|
||||||
|
graphics.fillRect(0, height - FLASH_BORDER_SIZE, width, FLASH_BORDER_SIZE);
|
||||||
|
graphics.fillRect(0, FLASH_BORDER_SIZE, FLASH_BORDER_SIZE, height - 2 * FLASH_BORDER_SIZE);
|
||||||
|
graphics.fillRect(width - FLASH_BORDER_SIZE, FLASH_BORDER_SIZE, FLASH_BORDER_SIZE, height - 2 * FLASH_BORDER_SIZE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -41,17 +41,23 @@ public class ItemChargesOverlay extends WidgetItemOverlay
|
|||||||
@Override
|
@Override
|
||||||
public void renderItemOverlay(Graphics2D g, int itemId, WidgetItem widgetItem)
|
public void renderItemOverlay(Graphics2D g, int itemId, WidgetItem widgetItem)
|
||||||
{
|
{
|
||||||
if (!config.itemCharges())
|
if (!itemCharges.isEnabled())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if on bank and if bank display is allowed
|
// Check if on bank or equipment and if display is allowed
|
||||||
boolean showBank = plugin.getHelperValue("itemChargesBank", Boolean.class, true);
|
boolean showBank = plugin.getHelperValue("itemInformationChargesBank", Boolean.class,
|
||||||
if (!showBank && widgetItem.getWidget() != null)
|
plugin.getHelperValue("itemChargesBank", Boolean.class, true));
|
||||||
|
boolean showEquipment = plugin.getHelperValue("itemInformationEquipment", Boolean.class, true);
|
||||||
|
if (widgetItem.getWidget() != null)
|
||||||
{
|
{
|
||||||
int group = net.runelite.api.widgets.WidgetUtil.componentToInterface(widgetItem.getWidget().getId());
|
int group = net.runelite.api.widgets.WidgetUtil.componentToInterface(widgetItem.getWidget().getId());
|
||||||
if (group == net.runelite.api.gameval.InterfaceID.BANKMAIN || group == 12) // 12 = InterfaceID.BANK
|
if (!showBank && (group == net.runelite.api.gameval.InterfaceID.BANKMAIN || group == 12)) // 12 = InterfaceID.BANK
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!showEquipment && (group == net.runelite.api.gameval.InterfaceID.WORNITEMS || group == 387 || group == 84))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,7 +71,8 @@ public class ItemChargesOverlay extends WidgetItemOverlay
|
|||||||
|
|
||||||
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
|
||||||
TimerTextSize size = plugin.getHelperValue("itemChargesSize", TimerTextSize.class, TimerTextSize.SMALL);
|
TimerTextSize size = plugin.getHelperValue("itemInformationSize", TimerTextSize.class,
|
||||||
|
plugin.getHelperValue("itemChargesSize", TimerTextSize.class, TimerTextSize.SMALL));
|
||||||
Font font = (size == TimerTextSize.SMALL) ? FontManager.getRunescapeSmallFont() : size.font();
|
Font font = (size == TimerTextSize.SMALL) ? FontManager.getRunescapeSmallFont() : size.font();
|
||||||
g.setFont(font);
|
g.setFont(font);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import com.example.essentialsplus.EssentialsPlusPlugin;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.ItemComposition;
|
||||||
|
import net.runelite.api.MenuEntry;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
|
import net.runelite.api.widgets.WidgetUtil;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.ui.overlay.Overlay;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayLayer;
|
||||||
|
import net.runelite.client.ui.overlay.OverlayPosition;
|
||||||
|
import net.runelite.client.ui.overlay.tooltip.Tooltip;
|
||||||
|
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
|
||||||
|
import net.runelite.client.util.ColorUtil;
|
||||||
|
import net.runelite.client.util.QuantityFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified hover tooltip overlay showing GE price, High Alchemy value, and Banked quantity
|
||||||
|
* for inventory and (optionally) bank items hovered by the mouse.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class ItemInformationOverlay extends Overlay
|
||||||
|
{
|
||||||
|
private static final int INVENTORY = 149;
|
||||||
|
private static final int BANK_INVENTORY = 15;
|
||||||
|
private static final Color GE = new Color(0xFFDC33);
|
||||||
|
private static final Color HA = new Color(0xFF9040);
|
||||||
|
private static final Color BANKED = new Color(0x90C0E0);
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final EssentialsPlusPlugin plugin;
|
||||||
|
private final ItemManager itemManager;
|
||||||
|
private final TooltipManager tooltipManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ItemInformationOverlay(Client client, EssentialsPlusConfig config, EssentialsPlusPlugin plugin,
|
||||||
|
ItemManager itemManager, TooltipManager tooltipManager)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
this.config = config;
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.itemManager = itemManager;
|
||||||
|
this.tooltipManager = tooltipManager;
|
||||||
|
setPosition(OverlayPosition.DYNAMIC);
|
||||||
|
setLayer(OverlayLayer.ABOVE_WIDGETS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension render(Graphics2D g)
|
||||||
|
{
|
||||||
|
if (plugin == null || !plugin.isItemInformationEnabled() || (client != null && client.isMenuOpen()))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean showGe = plugin.getHelperValue("itemInformationGe", Boolean.class, true);
|
||||||
|
boolean showHa = plugin.getHelperValue("itemInformationHa", Boolean.class, true);
|
||||||
|
boolean showBanked = plugin.getHelperValue("itemInformationBanked", Boolean.class, true);
|
||||||
|
|
||||||
|
if (!showGe && !showHa && !showBanked)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuEntry[] entries = client.getMenuEntries();
|
||||||
|
if (entries == null || entries.length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget w = entries[entries.length - 1].getWidget();
|
||||||
|
if (w == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int group = WidgetUtil.componentToInterface(w.getId());
|
||||||
|
boolean inInventory = (group == INVENTORY || group == BANK_INVENTORY);
|
||||||
|
boolean inBank = (group == InterfaceID.BANKMAIN || group == 12);
|
||||||
|
|
||||||
|
if (!inInventory)
|
||||||
|
{
|
||||||
|
boolean allowBank = plugin.getHelperValue("itemInformationBank", Boolean.class, true);
|
||||||
|
if (!inBank || !allowBank)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = w.getItemId();
|
||||||
|
if (id <= 0 || id == ItemID.COINS || id == 995 || id == ItemID.PLATINUM || id == 13204)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
long qty = Math.max(1, w.getItemQuantity());
|
||||||
|
int canonicalId = canonicalize(id);
|
||||||
|
String text = buildTooltipText(canonicalId, qty, showGe, showHa, showBanked);
|
||||||
|
if (text != null && !text.isEmpty() && tooltipManager != null)
|
||||||
|
{
|
||||||
|
tooltipManager.add(new Tooltip(text));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int canonicalize(int id)
|
||||||
|
{
|
||||||
|
return itemManager != null ? itemManager.canonicalize(id) : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getGePrice(int id)
|
||||||
|
{
|
||||||
|
return itemManager != null ? itemManager.getItemPrice(id) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getHaPrice(int id)
|
||||||
|
{
|
||||||
|
if (itemManager == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ItemComposition comp = itemManager.getItemComposition(id);
|
||||||
|
return comp == null ? 0 : comp.getHaPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildTooltipText(int canonicalId, long qty, boolean showGe, boolean showHa, boolean showBanked)
|
||||||
|
{
|
||||||
|
StringBuilder priceSb = new StringBuilder();
|
||||||
|
if (showGe)
|
||||||
|
{
|
||||||
|
long ge = getGePrice(canonicalId) * qty;
|
||||||
|
if (ge > 0)
|
||||||
|
{
|
||||||
|
priceSb.append("GE: ").append(ColorUtil.wrapWithColorTag(gp(ge), GE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showHa)
|
||||||
|
{
|
||||||
|
long ha = getHaPrice(canonicalId) * qty;
|
||||||
|
if (ha > 0)
|
||||||
|
{
|
||||||
|
if (priceSb.length() > 0)
|
||||||
|
{
|
||||||
|
priceSb.append(" ");
|
||||||
|
}
|
||||||
|
priceSb.append("HA: ").append(ColorUtil.wrapWithColorTag(gp(ha), HA));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String bankedStr = null;
|
||||||
|
if (showBanked && plugin != null)
|
||||||
|
{
|
||||||
|
long bankedQty = plugin.getBankedQuantity(canonicalId);
|
||||||
|
if (bankedQty >= 0)
|
||||||
|
{
|
||||||
|
bankedStr = "Banked: " + ColorUtil.wrapWithColorTag(QuantityFormatter.formatNumber(bankedQty), BANKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder full = new StringBuilder();
|
||||||
|
if (priceSb.length() > 0)
|
||||||
|
{
|
||||||
|
full.append(priceSb);
|
||||||
|
}
|
||||||
|
if (bankedStr != null)
|
||||||
|
{
|
||||||
|
if (full.length() > 0)
|
||||||
|
{
|
||||||
|
full.append("</br>");
|
||||||
|
}
|
||||||
|
full.append(bankedStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return full.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String gp(long v)
|
||||||
|
{
|
||||||
|
return QuantityFormatter.quantityToStackSize(v) + " gp";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,114 +1,24 @@
|
|||||||
package com.example.essentialsplus.helpers;
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
import com.example.essentialsplus.EssentialsPlusConfig;
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
import java.awt.Color;
|
import com.example.essentialsplus.EssentialsPlusPlugin;
|
||||||
import java.awt.Dimension;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
import net.runelite.api.Client;
|
import net.runelite.api.Client;
|
||||||
import net.runelite.api.ItemComposition;
|
|
||||||
import net.runelite.api.MenuEntry;
|
|
||||||
import net.runelite.api.gameval.ItemID;
|
|
||||||
import net.runelite.api.widgets.Widget;
|
|
||||||
import net.runelite.api.widgets.WidgetUtil;
|
|
||||||
import net.runelite.client.game.ItemManager;
|
import net.runelite.client.game.ItemManager;
|
||||||
import net.runelite.client.ui.overlay.Overlay;
|
|
||||||
import net.runelite.client.ui.overlay.OverlayLayer;
|
|
||||||
import net.runelite.client.ui.overlay.OverlayPosition;
|
|
||||||
import net.runelite.client.ui.overlay.tooltip.Tooltip;
|
|
||||||
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
|
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
|
||||||
import net.runelite.client.util.ColorUtil;
|
|
||||||
import net.runelite.client.util.QuantityFormatter;
|
|
||||||
|
|
||||||
/** Tooltip line with the GE and/or high alch value of the inventory item under the mouse. */
|
/**
|
||||||
public class ItemPriceOverlay extends Overlay
|
* Legacy wrapper for {@link ItemInformationOverlay}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
@Singleton
|
||||||
|
public class ItemPriceOverlay extends ItemInformationOverlay
|
||||||
{
|
{
|
||||||
private static final int INVENTORY = 149;
|
|
||||||
private static final int BANK_INVENTORY = 15;
|
|
||||||
private static final Color GE = new Color(0xFFDC33);
|
|
||||||
private static final Color HA = new Color(0xFF9040);
|
|
||||||
|
|
||||||
private final Client client;
|
|
||||||
private final EssentialsPlusConfig config;
|
|
||||||
private final ItemManager itemManager;
|
|
||||||
private final TooltipManager tooltipManager;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ItemPriceOverlay(Client client, EssentialsPlusConfig config, ItemManager itemManager, TooltipManager tooltipManager)
|
ItemPriceOverlay(Client client, EssentialsPlusConfig config, EssentialsPlusPlugin plugin,
|
||||||
|
ItemManager itemManager, TooltipManager tooltipManager)
|
||||||
{
|
{
|
||||||
this.client = client;
|
super(client, config, plugin, itemManager, tooltipManager);
|
||||||
this.config = config;
|
|
||||||
this.itemManager = itemManager;
|
|
||||||
this.tooltipManager = tooltipManager;
|
|
||||||
setPosition(OverlayPosition.DYNAMIC);
|
|
||||||
setLayer(OverlayLayer.ABOVE_WIDGETS);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dimension render(Graphics2D g)
|
|
||||||
{
|
|
||||||
if (!config.itemPrices() || (!config.itemPricesGe() && !config.itemPricesHa()) || client.isMenuOpen())
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
MenuEntry[] entries = client.getMenuEntries();
|
|
||||||
if (entries.length == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Widget w = entries[entries.length - 1].getWidget();
|
|
||||||
if (w == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int group = WidgetUtil.componentToInterface(w.getId());
|
|
||||||
if (group != INVENTORY && group != BANK_INVENTORY)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int id = w.getItemId();
|
|
||||||
if (id <= 0 || id == ItemID.COINS || id == ItemID.PLATINUM)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
long qty = Math.max(1, w.getItemQuantity());
|
|
||||||
String text = text(itemManager.canonicalize(id), qty);
|
|
||||||
if (!text.isEmpty())
|
|
||||||
{
|
|
||||||
tooltipManager.add(new Tooltip(text));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String text(int id, long qty)
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
if (config.itemPricesGe())
|
|
||||||
{
|
|
||||||
long ge = (long) itemManager.getItemPrice(id) * qty;
|
|
||||||
if (ge > 0)
|
|
||||||
{
|
|
||||||
sb.append("GE: ").append(ColorUtil.wrapWithColorTag(gp(ge), GE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (config.itemPricesHa())
|
|
||||||
{
|
|
||||||
ItemComposition comp = itemManager.getItemComposition(id);
|
|
||||||
long ha = comp == null ? 0 : (long) comp.getHaPrice() * qty;
|
|
||||||
if (ha > 0)
|
|
||||||
{
|
|
||||||
if (sb.length() > 0)
|
|
||||||
{
|
|
||||||
sb.append(" ");
|
|
||||||
}
|
|
||||||
sb.append("HA: ").append(ColorUtil.wrapWithColorTag(gp(ha), HA));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String gp(long v)
|
|
||||||
{
|
|
||||||
return QuantityFormatter.quantityToStackSize(v) + " gp";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,581 @@
|
|||||||
|
package com.example.essentialsplus.helpers;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.ChatMessageType;
|
||||||
|
import net.runelite.api.Friend;
|
||||||
|
import net.runelite.api.GameState;
|
||||||
|
import net.runelite.api.MenuAction;
|
||||||
|
import net.runelite.api.NameableContainer;
|
||||||
|
import net.runelite.api.Skill;
|
||||||
|
import net.runelite.api.World;
|
||||||
|
import net.runelite.api.events.GameStateChanged;
|
||||||
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.MenuEntryAdded;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.widgets.WidgetUtil;
|
||||||
|
import net.runelite.client.RuneLite;
|
||||||
|
import net.runelite.client.callback.ClientThread;
|
||||||
|
import net.runelite.client.chat.ChatMessageBuilder;
|
||||||
|
import net.runelite.client.chat.ChatMessageManager;
|
||||||
|
import net.runelite.client.chat.QueuedMessage;
|
||||||
|
import net.runelite.client.eventbus.Subscribe;
|
||||||
|
import net.runelite.client.game.WorldService;
|
||||||
|
import net.runelite.client.input.KeyManager;
|
||||||
|
import net.runelite.client.util.HotkeyListener;
|
||||||
|
import net.runelite.client.util.Text;
|
||||||
|
import net.runelite.client.util.WorldUtil;
|
||||||
|
import net.runelite.http.api.worlds.WorldResult;
|
||||||
|
import net.runelite.http.api.worlds.WorldType;
|
||||||
|
|
||||||
|
/** Tracks world-hop attempts across RuneLite accounts on this installation. */
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class WorldHopTracker
|
||||||
|
{
|
||||||
|
private static final long HOUR_MILLIS = 60L * 60L * 1000L;
|
||||||
|
private static final long RETENTION_MILLIS = 48L * HOUR_MILLIS;
|
||||||
|
private static final int MAX_PLAYER_COUNT = 1950;
|
||||||
|
private static final int MAX_SWITCHER_ATTEMPTS = 3;
|
||||||
|
private static final Path HISTORY_FILE = RuneLite.RUNELITE_DIR.toPath()
|
||||||
|
.resolve("plugin-data")
|
||||||
|
.resolve("essentialsplus")
|
||||||
|
.resolve("world-hops.txt");
|
||||||
|
|
||||||
|
private final EssentialsPlusConfig config;
|
||||||
|
private final ChatMessageManager chatMessageManager;
|
||||||
|
private final HotkeyListener nextWorldListener;
|
||||||
|
private final HotkeyListener randomWorldListener;
|
||||||
|
private final Deque<Long> timestamps = new ArrayDeque<>();
|
||||||
|
private int pendingHopCount;
|
||||||
|
private int pendingDailyHopCount;
|
||||||
|
private World pendingHopTarget;
|
||||||
|
private int switcherAttempts;
|
||||||
|
private boolean hotkeysRegistered;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Client client;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ClientThread clientThread;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private KeyManager keyManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private WorldService worldService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public WorldHopTracker(EssentialsPlusConfig config, ChatMessageManager chatMessageManager)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
this.chatMessageManager = chatMessageManager;
|
||||||
|
nextWorldListener = new HotkeyListener(config::worldHopNextKey)
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void keyPressed(java.awt.event.KeyEvent e)
|
||||||
|
{
|
||||||
|
if (config.worldHopTracker())
|
||||||
|
{
|
||||||
|
super.keyPressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hotkeyPressed()
|
||||||
|
{
|
||||||
|
clientThread.invoke(() -> hopToNextWorld(false));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
randomWorldListener = new HotkeyListener(config::worldHopRandomKey)
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void keyPressed(java.awt.event.KeyEvent e)
|
||||||
|
{
|
||||||
|
if (config.worldHopTracker())
|
||||||
|
{
|
||||||
|
super.keyPressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hotkeyPressed()
|
||||||
|
{
|
||||||
|
clientThread.invoke(() -> hopToNextWorld(true));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onMenuEntryAdded(MenuEntryAdded event)
|
||||||
|
{
|
||||||
|
if (!config.worldHopTracker()
|
||||||
|
|| client.getGameState() != GameState.LOGGED_IN
|
||||||
|
|| WidgetUtil.componentToInterface(event.getActionParam1()) != InterfaceID.FRIENDS
|
||||||
|
|| !"Remove friend".equals(event.getOption()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Friend friend = findFriend(event.getTarget());
|
||||||
|
if (friend == null || getHopToWorld(friend.getWorld()) == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.createMenuEntry(-1)
|
||||||
|
.setOption("Hop-to")
|
||||||
|
.setTarget(event.getTarget())
|
||||||
|
.setType(MenuAction.RUNELITE)
|
||||||
|
.onClick(e -> clientThread.invoke(() -> hopToFriend(e.getTarget())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Friend findFriend(String name)
|
||||||
|
{
|
||||||
|
if (name == null || name.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
NameableContainer<Friend> friends = client.getFriendContainer();
|
||||||
|
return friends == null ? null : friends.findByName(Text.removeTags(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hopToFriend(String name)
|
||||||
|
{
|
||||||
|
Friend friend = findFriend(name);
|
||||||
|
if (friend == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
net.runelite.http.api.worlds.World target = getHopToWorld(friend.getWorld());
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueWorldHop(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private net.runelite.http.api.worlds.World getHopToWorld(int worldId)
|
||||||
|
{
|
||||||
|
if (!config.worldHopTracker()
|
||||||
|
|| client.getGameState() != GameState.LOGGED_IN
|
||||||
|
|| worldId <= 0
|
||||||
|
|| worldId == client.getWorld())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldResult result = worldService.getWorlds();
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
net.runelite.http.api.worlds.World current = result.findWorld(client.getWorld());
|
||||||
|
net.runelite.http.api.worlds.World target = result.findWorld(worldId);
|
||||||
|
if (current == null || target == null || target.getPlayers() < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnumSet<WorldType> targetTypes = target.getTypes();
|
||||||
|
if (targetTypes.contains(WorldType.BOUNTY)
|
||||||
|
|| (!current.getTypes().contains(WorldType.PVP) && targetTypes.contains(WorldType.PVP)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (config.worldHopAvoidDangerous()
|
||||||
|
&& (targetTypes.contains(WorldType.PVP)
|
||||||
|
|| targetTypes.contains(WorldType.PVP_ARENA)
|
||||||
|
|| targetTypes.contains(WorldType.HIGH_RISK)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalLevel = client.getTotalLevel();
|
||||||
|
int f2pTotalLevel = Arrays.stream(Skill.values())
|
||||||
|
.filter(skill -> !skill.isMembers())
|
||||||
|
.mapToInt(client::getRealSkillLevel)
|
||||||
|
.sum();
|
||||||
|
return meetsSkillTotalRequirement(target, totalLevel, f2pTotalLevel) ? target : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerHotkeys()
|
||||||
|
{
|
||||||
|
if (hotkeysRegistered)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keyManager.registerKeyListener(nextWorldListener);
|
||||||
|
keyManager.registerKeyListener(randomWorldListener);
|
||||||
|
hotkeysRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterHotkeys()
|
||||||
|
{
|
||||||
|
if (!hotkeysRegistered)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keyManager.unregisterKeyListener(nextWorldListener);
|
||||||
|
keyManager.unregisterKeyListener(randomWorldListener);
|
||||||
|
hotkeysRegistered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void loadHistory()
|
||||||
|
{
|
||||||
|
timestamps.clear();
|
||||||
|
if (Files.exists(HISTORY_FILE))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (String line : Files.readAllLines(HISTORY_FILE, StandardCharsets.UTF_8))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
timestamps.addLast(Long.parseLong(line.trim()));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ignored)
|
||||||
|
{
|
||||||
|
// Ignore damaged or incomplete entries.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prune(System.currentTimeMillis());
|
||||||
|
persist();
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
log.warn("Unable to load world-hop history", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public synchronized void onGameStateChanged(GameStateChanged event)
|
||||||
|
{
|
||||||
|
GameState state = event.getGameState();
|
||||||
|
if (state != GameState.LOGGED_IN && state != GameState.HOPPING)
|
||||||
|
{
|
||||||
|
pendingHopTarget = null;
|
||||||
|
}
|
||||||
|
if (state == GameState.HOPPING)
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
prune(now);
|
||||||
|
timestamps.addLast(now);
|
||||||
|
persist();
|
||||||
|
pendingHopCount = countSince(now - HOUR_MILLIS);
|
||||||
|
pendingDailyHopCount = countToday(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingHopCount > 0 && (state == GameState.LOGGED_IN
|
||||||
|
|| state == GameState.LOGIN_SCREEN || state == GameState.LOGIN_SCREEN_AUTHENTICATOR))
|
||||||
|
{
|
||||||
|
if (config.worldHopTracker())
|
||||||
|
{
|
||||||
|
String message = new ChatMessageBuilder()
|
||||||
|
.append(new Color(0xFF9800), "Hop Tracker " + pendingHopCount + "/h | " + pendingDailyHopCount + "/day")
|
||||||
|
.build();
|
||||||
|
chatMessageManager.queue(QueuedMessage.builder()
|
||||||
|
.type(ChatMessageType.CONSOLE)
|
||||||
|
.runeLiteFormattedMessage(message)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
pendingHopCount = 0;
|
||||||
|
pendingDailyHopCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onGameTick(GameTick event)
|
||||||
|
{
|
||||||
|
if (pendingHopTarget == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
pendingHopTarget = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.getWidget(InterfaceID.Worldswitcher.BUTTONS) == null)
|
||||||
|
{
|
||||||
|
client.openWorldHopper();
|
||||||
|
if (++switcherAttempts >= MAX_SWITCHER_ATTEMPTS)
|
||||||
|
{
|
||||||
|
pendingHopTarget = null;
|
||||||
|
queueStatus("Couldn't open the world switcher.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.hopToWorld(pendingHopTarget);
|
||||||
|
pendingHopTarget = null;
|
||||||
|
switcherAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hopToNextWorld(boolean random)
|
||||||
|
{
|
||||||
|
if (!config.worldHopTracker() || client.getGameState() != GameState.LOGGED_IN)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldResult result = worldService.getWorlds();
|
||||||
|
if (result == null || result.getWorlds().isEmpty())
|
||||||
|
{
|
||||||
|
worldService.refresh();
|
||||||
|
queueStatus("World list is loading.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
net.runelite.http.api.worlds.World current = result.findWorld(client.getWorld());
|
||||||
|
if (current == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<net.runelite.http.api.worlds.World> worlds = new ArrayList<>(result.getWorlds());
|
||||||
|
worlds.sort(Comparator.comparingInt(net.runelite.http.api.worlds.World::getId));
|
||||||
|
int totalLevel = client.getTotalLevel();
|
||||||
|
int f2pTotalLevel = Arrays.stream(Skill.values())
|
||||||
|
.filter(skill -> !skill.isMembers())
|
||||||
|
.mapToInt(client::getRealSkillLevel)
|
||||||
|
.sum();
|
||||||
|
EnumSet<WorldType> currentTypes = normalizedTypes(current, totalLevel, f2pTotalLevel);
|
||||||
|
List<net.runelite.http.api.worlds.World> candidates = new ArrayList<>();
|
||||||
|
for (net.runelite.http.api.worlds.World world : worlds)
|
||||||
|
{
|
||||||
|
if (world.getId() != client.getWorld()
|
||||||
|
&& isAvailable(world)
|
||||||
|
&& meetsSkillTotalRequirement(world, totalLevel, f2pTotalLevel)
|
||||||
|
&& normalizedTypes(world, totalLevel, f2pTotalLevel).equals(currentTypes))
|
||||||
|
{
|
||||||
|
candidates.add(world);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.isEmpty())
|
||||||
|
{
|
||||||
|
queueStatus("Couldn't find an eligible world.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
net.runelite.http.api.worlds.World target;
|
||||||
|
if (random)
|
||||||
|
{
|
||||||
|
target = candidates.get(ThreadLocalRandom.current().nextInt(candidates.size()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int currentIndex = -1;
|
||||||
|
for (int i = 0; i < worlds.size(); i++)
|
||||||
|
{
|
||||||
|
if (worlds.get(i).getId() == client.getWorld())
|
||||||
|
{
|
||||||
|
currentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target = null;
|
||||||
|
for (int step = 1; step <= worlds.size(); step++)
|
||||||
|
{
|
||||||
|
net.runelite.http.api.worlds.World candidate = worlds.get((currentIndex + step) % worlds.size());
|
||||||
|
if (candidates.contains(candidate))
|
||||||
|
{
|
||||||
|
target = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
queueStatus("Couldn't find an eligible world.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queueWorldHop(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queueWorldHop(net.runelite.http.api.worlds.World target)
|
||||||
|
{
|
||||||
|
World hopTarget = client.createWorld();
|
||||||
|
hopTarget.setId(target.getId());
|
||||||
|
hopTarget.setAddress(target.getAddress());
|
||||||
|
hopTarget.setActivity(target.getActivity());
|
||||||
|
hopTarget.setLocation(target.getLocation());
|
||||||
|
hopTarget.setPlayerCount(target.getPlayers());
|
||||||
|
hopTarget.setTypes(WorldUtil.toWorldTypes(target.getTypes()));
|
||||||
|
pendingHopTarget = hopTarget;
|
||||||
|
switcherAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EnumSet<WorldType> normalizedTypes(net.runelite.http.api.worlds.World world, int totalLevel, int f2pTotalLevel)
|
||||||
|
{
|
||||||
|
EnumSet<WorldType> types = EnumSet.noneOf(WorldType.class);
|
||||||
|
types.addAll(world.getTypes());
|
||||||
|
types.remove(WorldType.BOUNTY);
|
||||||
|
if (types.contains(WorldType.SKILL_TOTAL) && meetsSkillTotalRequirement(world, totalLevel, f2pTotalLevel))
|
||||||
|
{
|
||||||
|
types.remove(WorldType.SKILL_TOTAL);
|
||||||
|
}
|
||||||
|
types.remove(WorldType.LAST_MAN_STANDING);
|
||||||
|
if (config.worldHopAvoidDangerous())
|
||||||
|
{
|
||||||
|
types.remove(WorldType.PVP);
|
||||||
|
types.remove(WorldType.PVP_ARENA);
|
||||||
|
types.remove(WorldType.HIGH_RISK);
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean meetsSkillTotalRequirement(net.runelite.http.api.worlds.World world, int totalLevel, int f2pTotalLevel)
|
||||||
|
{
|
||||||
|
EnumSet<WorldType> types = world.getTypes();
|
||||||
|
if (!types.contains(WorldType.SKILL_TOTAL))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String activity = world.getActivity();
|
||||||
|
int separator = activity == null ? -1 : activity.indexOf(' ');
|
||||||
|
if (separator <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int requiredTotal;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
requiredTotal = Integer.parseInt(activity.substring(0, separator));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ex)
|
||||||
|
{
|
||||||
|
log.warn("Failed to parse skill total requirement for world {}", world.getId(), ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int effectiveTotalLevel = types.contains(WorldType.MEMBERS) ? totalLevel : f2pTotalLevel;
|
||||||
|
return effectiveTotalLevel >= requiredTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAvailable(net.runelite.http.api.worlds.World world)
|
||||||
|
{
|
||||||
|
EnumSet<WorldType> types = world.getTypes();
|
||||||
|
if (world.getPlayers() < 0 || world.getPlayers() >= MAX_PLAYER_COUNT || types.contains(WorldType.BOUNTY))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !config.worldHopAvoidDangerous()
|
||||||
|
|| (!types.contains(WorldType.PVP) && !types.contains(WorldType.PVP_ARENA) && !types.contains(WorldType.HIGH_RISK));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queueStatus(String message)
|
||||||
|
{
|
||||||
|
chatMessageManager.queue(QueuedMessage.builder()
|
||||||
|
.type(ChatMessageType.CONSOLE)
|
||||||
|
.value("Hop Tracker: " + message)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countToday(long now)
|
||||||
|
{
|
||||||
|
ZoneId zone = ZoneId.systemDefault();
|
||||||
|
long startOfDay = Instant.ofEpochMilli(now)
|
||||||
|
.atZone(zone)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay(zone)
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli();
|
||||||
|
return countSince(startOfDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prune(long now)
|
||||||
|
{
|
||||||
|
long cutoff = now - RETENTION_MILLIS;
|
||||||
|
timestamps.removeIf(timestamp -> timestamp < cutoff || timestamp > now);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int countSince(long cutoff)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (long timestamp : timestamps)
|
||||||
|
{
|
||||||
|
if (timestamp >= cutoff)
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persist()
|
||||||
|
{
|
||||||
|
Path temporaryFile = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.createDirectories(HISTORY_FILE.getParent());
|
||||||
|
List<String> lines = new ArrayList<>(timestamps.size());
|
||||||
|
for (long timestamp : timestamps)
|
||||||
|
{
|
||||||
|
lines.add(Long.toString(timestamp));
|
||||||
|
}
|
||||||
|
temporaryFile = Files.createTempFile(HISTORY_FILE.getParent(), "world-hops-", ".tmp");
|
||||||
|
Files.write(temporaryFile, lines, StandardCharsets.UTF_8);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.move(temporaryFile, HISTORY_FILE, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
catch (AtomicMoveNotSupportedException ignored)
|
||||||
|
{
|
||||||
|
Files.move(temporaryFile, HISTORY_FILE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
log.warn("Unable to save world-hop history", ex);
|
||||||
|
if (temporaryFile != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.deleteIfExists(temporaryFile);
|
||||||
|
}
|
||||||
|
catch (IOException ignored)
|
||||||
|
{
|
||||||
|
// Keep the original persistence error as the useful diagnostic.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ public class LootRecord
|
|||||||
this.lastSeen = this.firstSeen;
|
this.lastSeen = this.firstSeen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void addDrop(int itemId, String itemName, int quantity, int gePrice)
|
public synchronized void addDrop(int itemId, String itemName, int quantity, long gePrice)
|
||||||
{
|
{
|
||||||
this.lastSeen = System.currentTimeMillis();
|
this.lastSeen = System.currentTimeMillis();
|
||||||
currentEvent(lastSeen).i.merge(itemId, (long) quantity, Long::sum);
|
currentEvent(lastSeen).i.merge(itemId, (long) quantity, Long::sum);
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ public class LootTrackerStore
|
|||||||
int qty = item.getQuantity();
|
int qty = item.getQuantity();
|
||||||
|
|
||||||
String name = "Item " + id;
|
String name = "Item " + id;
|
||||||
int gePrice = 0;
|
long gePrice = 0;
|
||||||
if (itemManager != null)
|
if (itemManager != null)
|
||||||
{
|
{
|
||||||
ItemComposition comp = itemManager.getItemComposition(id);
|
ItemComposition comp = itemManager.getItemComposition(id);
|
||||||
|
|||||||
@@ -870,7 +870,7 @@ public class CombatTracker
|
|||||||
|
|
||||||
String name = comp.getName();
|
String name = comp.getName();
|
||||||
String lower = name.toLowerCase(Locale.ENGLISH);
|
String lower = name.toLowerCase(Locale.ENGLISH);
|
||||||
int price = Math.max(0, itemManager.getItemPrice(itemId));
|
long price = Math.max(0, itemManager.getItemPrice(itemId));
|
||||||
|
|
||||||
// Check inventory actions for "Eat" or "Drink"
|
// Check inventory actions for "Eat" or "Drink"
|
||||||
String[] actions = comp.getInventoryActions();
|
String[] actions = comp.getInventoryActions();
|
||||||
|
|||||||
@@ -0,0 +1,505 @@
|
|||||||
|
package com.example.essentialsplus.player;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import net.runelite.api.ItemComposition;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
import net.runelite.api.PlayerComposition;
|
||||||
|
import net.runelite.api.kit.KitType;
|
||||||
|
import net.runelite.client.game.ItemManager;
|
||||||
|
import net.runelite.client.game.SkillIconManager;
|
||||||
|
import net.runelite.client.hiscore.HiscoreEndpoint;
|
||||||
|
import net.runelite.client.hiscore.HiscoreManager;
|
||||||
|
import net.runelite.client.hiscore.HiscoreResult;
|
||||||
|
import net.runelite.client.hiscore.HiscoreSkill;
|
||||||
|
import net.runelite.client.hiscore.HiscoreSkillType;
|
||||||
|
import net.runelite.client.hiscore.Skill;
|
||||||
|
|
||||||
|
/** Captures visible player equipment and formats it with public hiscore data. */
|
||||||
|
@Singleton
|
||||||
|
public class PlayerEquipmentFeature
|
||||||
|
{
|
||||||
|
private static final HiscoreEndpoint[] HISCORE_ENDPOINTS = {
|
||||||
|
HiscoreEndpoint.NORMAL,
|
||||||
|
HiscoreEndpoint.IRONMAN,
|
||||||
|
HiscoreEndpoint.HARDCORE_IRONMAN,
|
||||||
|
HiscoreEndpoint.ULTIMATE_IRONMAN,
|
||||||
|
HiscoreEndpoint.DEADMAN,
|
||||||
|
HiscoreEndpoint.SEASONAL,
|
||||||
|
HiscoreEndpoint.TOURNAMENT,
|
||||||
|
HiscoreEndpoint.FRESH_START_WORLD,
|
||||||
|
HiscoreEndpoint.PURE,
|
||||||
|
HiscoreEndpoint.LEVEL_3_SKILLER
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final HiscoreSkill[] SKILLS = {
|
||||||
|
HiscoreSkill.ATTACK, HiscoreSkill.STRENGTH, HiscoreSkill.DEFENCE, HiscoreSkill.RANGED,
|
||||||
|
HiscoreSkill.PRAYER, HiscoreSkill.MAGIC, HiscoreSkill.RUNECRAFT, HiscoreSkill.CONSTRUCTION,
|
||||||
|
HiscoreSkill.HITPOINTS, HiscoreSkill.AGILITY, HiscoreSkill.HERBLORE, HiscoreSkill.THIEVING,
|
||||||
|
HiscoreSkill.CRAFTING, HiscoreSkill.FLETCHING, HiscoreSkill.SLAYER, HiscoreSkill.HUNTER,
|
||||||
|
HiscoreSkill.MINING, HiscoreSkill.SMITHING, HiscoreSkill.FISHING, HiscoreSkill.COOKING,
|
||||||
|
HiscoreSkill.FIREMAKING, HiscoreSkill.WOODCUTTING, HiscoreSkill.FARMING, HiscoreSkill.SAILING
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String[] SLOT_NAMES = {
|
||||||
|
"Head", "Cape", "Amulet", "Weapon", "Body", "Shield", "Arms", "Legs", "Hair", "Hands", "Boots", "Jaw"
|
||||||
|
};
|
||||||
|
private static final DateTimeFormatter FETCHED_AT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss", Locale.ROOT)
|
||||||
|
.withZone(ZoneId.systemDefault());
|
||||||
|
private static final String LOCAL_IMAGE_BASE = "https://essentials-plus.invalid/images/";
|
||||||
|
|
||||||
|
private final ItemManager itemManager;
|
||||||
|
private final HiscoreManager hiscoreManager;
|
||||||
|
private final SkillIconManager skillIconManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PlayerEquipmentFeature(ItemManager itemManager, HiscoreManager hiscoreManager, SkillIconManager skillIconManager)
|
||||||
|
{
|
||||||
|
this.itemManager = itemManager;
|
||||||
|
this.hiscoreManager = hiscoreManager;
|
||||||
|
this.skillIconManager = skillIconManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Snapshot capture(Player player)
|
||||||
|
{
|
||||||
|
String name = cleanName(player.getName());
|
||||||
|
List<EquipmentItem> equipment = new ArrayList<>();
|
||||||
|
Map<String, BufferedImage> images = skillImages();
|
||||||
|
PlayerComposition composition = player.getPlayerComposition();
|
||||||
|
int[] appearance = composition == null ? null : composition.getEquipmentIds();
|
||||||
|
if (appearance != null)
|
||||||
|
{
|
||||||
|
for (KitType slot : KitType.values())
|
||||||
|
{
|
||||||
|
int index = slot.getIndex();
|
||||||
|
if (index < 0 || index >= appearance.length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int encodedId = appearance[index];
|
||||||
|
if (encodedId < PlayerComposition.ITEM_OFFSET)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int itemId = encodedId - PlayerComposition.ITEM_OFFSET;
|
||||||
|
ItemComposition item = itemManager.getItemComposition(itemId);
|
||||||
|
if (item == null || item.getName() == null || item.getName().trim().isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int priceId = itemManager.canonicalize(itemId);
|
||||||
|
ItemComposition pricedItem = itemManager.getItemComposition(priceId);
|
||||||
|
long ge = Math.max(0, itemManager.getItemPrice(priceId));
|
||||||
|
long ha = pricedItem == null ? 0 : Math.max(0, pricedItem.getHaPrice());
|
||||||
|
String icon = itemIcon(itemId);
|
||||||
|
images.put(icon, itemManager.getImage(itemId));
|
||||||
|
equipment.add(new EquipmentItem(slotName(slot), item.getName(), ge, ha, icon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Snapshot(name, player.getCombatLevel(), player.getHealthRatio(), player.getHealthScale(), equipment, images, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Snapshot for a name search: public hiscores are available, but no remote equipment data is exposed. */
|
||||||
|
public Snapshot captureHiscoreTarget(String playerName)
|
||||||
|
{
|
||||||
|
return new Snapshot(cleanName(playerName), 0, -1, -1, Collections.emptyList(), skillImages(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, BufferedImage> skillImages()
|
||||||
|
{
|
||||||
|
Map<String, BufferedImage> images = new ConcurrentHashMap<>();
|
||||||
|
for (HiscoreSkill skill : SKILLS)
|
||||||
|
{
|
||||||
|
BufferedImage image = skillIconManager.getSkillImage(net.runelite.api.Skill.valueOf(skill.name()), true);
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
images.put(skillIcon(skill), image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cleanName(String name)
|
||||||
|
{
|
||||||
|
String cleaned = name == null ? "" : net.runelite.client.util.Text.removeTags(name).trim();
|
||||||
|
return cleaned.isEmpty() ? "Unknown player" : cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HiscoreResult lookupHiscores(String playerName) throws IOException
|
||||||
|
{
|
||||||
|
for (HiscoreEndpoint endpoint : HISCORE_ENDPOINTS)
|
||||||
|
{
|
||||||
|
HiscoreResult result = hiscoreManager.lookup(playerName, endpoint);
|
||||||
|
if (result != null && !result.getSkills().isEmpty())
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String render(Snapshot snapshot, HiscoreResult hiscores, boolean loading, boolean failed, long fetchedAtMillis,
|
||||||
|
PlayerHiscoreHistory.Comparison comparison, PlayerHiscoreHistory.Range range)
|
||||||
|
{
|
||||||
|
StringBuilder html = new StringBuilder(7000);
|
||||||
|
appendEquipment(html, snapshot);
|
||||||
|
int combatLevel = snapshot.combatLevel > 0 ? snapshot.combatLevel : combatLevelFrom(hiscores);
|
||||||
|
html.append("<h2>Player</h2><table><tr><th>Combat level</th><td>")
|
||||||
|
.append(combatLevel > 0 ? combatLevel : "—")
|
||||||
|
.append(deltaMarkup(combatLevelDelta(hiscores, comparison)))
|
||||||
|
.append("</td></tr>");
|
||||||
|
|
||||||
|
Skill hitpoints = hiscores == null ? null : hiscores.getSkill(HiscoreSkill.HITPOINTS);
|
||||||
|
int hpLevel = hitpoints == null ? 0 : hitpoints.getLevel();
|
||||||
|
html.append("<tr><th>Hitpoints level</th><td>").append(hpLevel > 0 ? hpLevel : "—")
|
||||||
|
.append(deltaMarkup(comparison == null ? null : comparison.levelDelta(HiscoreSkill.HITPOINTS))).append("</td></tr>");
|
||||||
|
String liveHealth = liveHealth(snapshot, hpLevel);
|
||||||
|
html.append("<tr><th>Current health</th><td>").append(escape(liveHealth)).append("</td></tr>");
|
||||||
|
|
||||||
|
long totalXp = 0;
|
||||||
|
int totalLevel = 0;
|
||||||
|
if (hiscores != null)
|
||||||
|
{
|
||||||
|
for (HiscoreSkill skill : SKILLS)
|
||||||
|
{
|
||||||
|
Skill value = hiscores.getSkill(skill);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
if (value.getExperience() > 0)
|
||||||
|
{
|
||||||
|
totalXp += value.getExperience();
|
||||||
|
}
|
||||||
|
if (value.getLevel() > 0)
|
||||||
|
{
|
||||||
|
totalLevel += value.getLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Skill overall = hiscores.getSkill(HiscoreSkill.OVERALL);
|
||||||
|
if (overall != null)
|
||||||
|
{
|
||||||
|
if (overall.getExperience() >= 0)
|
||||||
|
{
|
||||||
|
totalXp = overall.getExperience();
|
||||||
|
}
|
||||||
|
if (overall.getLevel() > 0)
|
||||||
|
{
|
||||||
|
totalLevel = overall.getLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html.append("<tr><th>Last fetched</th><td>").append(fetchedAtMillis <= 0 ? (loading ? "Loading…" : "—") : FETCHED_AT.format(Instant.ofEpochMilli(fetchedAtMillis))).append("</td></tr>")
|
||||||
|
.append("<tr><th>Changes</th><td>").append(escape(comparisonLabel(comparison, range))).append("</td></tr>")
|
||||||
|
.append("<tr><th>Total level</th><td>").append(hiscores == null ? "—" : number(totalLevel))
|
||||||
|
.append(deltaMarkup(comparison == null ? null : comparison.levelDelta(HiscoreSkill.OVERALL))).append("</td></tr>")
|
||||||
|
.append("<tr><th>Total XP</th><td>").append(hiscores == null ? "—" : number(totalXp))
|
||||||
|
.append(deltaMarkup(comparison == null ? null : comparison.experienceDelta(HiscoreSkill.OVERALL))).append("</td></tr></table>");
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
{
|
||||||
|
html.append("<p>Loading public hiscores…</p>");
|
||||||
|
}
|
||||||
|
else if (hiscores == null)
|
||||||
|
{
|
||||||
|
html.append("<p>").append(failed ? "Could not load public hiscores." : "No public hiscores found.").append("</p>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
html.append("<p>Skill levels and XP come from public hiscores and may be out of date.</p>")
|
||||||
|
.append("<h2>Skills</h2><table><tr><th colspan=\"2\">Skill</th><th>Level</th><th>XP</th></tr>");
|
||||||
|
for (HiscoreSkill skill : SKILLS)
|
||||||
|
{
|
||||||
|
Skill value = hiscores.getSkill(skill);
|
||||||
|
html.append("<tr><td><img src=\"").append(skillIcon(skill)).append("\" width=\"20\" height=\"20\"></td><td>")
|
||||||
|
.append(escape(skill.getName())).append("</td><td>")
|
||||||
|
.append(value == null || value.getLevel() < 0 ? "—" : number(value.getLevel()))
|
||||||
|
.append(deltaMarkup(comparison == null ? null : comparison.levelDelta(skill))).append("</td><td>")
|
||||||
|
.append(value == null || value.getExperience() < 0 ? "—" : number(value.getExperience()))
|
||||||
|
.append(deltaMarkup(comparison == null ? null : comparison.experienceDelta(skill))).append("</td></tr>");
|
||||||
|
}
|
||||||
|
html.append("</table>");
|
||||||
|
appendBossKills(html, hiscores, comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendEquipment(StringBuilder html, Snapshot snapshot)
|
||||||
|
{
|
||||||
|
html.append("<h2>Visible equipment</h2>");
|
||||||
|
if (snapshot.equipment.isEmpty())
|
||||||
|
{
|
||||||
|
html.append(snapshot.publicSearchOnly
|
||||||
|
? "<p>Public hiscores don't include equipment; gear is only available for players visible nearby.</p>"
|
||||||
|
: "<p>No equipment is visible in this player's model.</p>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
html.append("<table><tr><th>Slot</th><th colspan=\"2\">Item</th><th>GE</th><th>HA</th></tr>");
|
||||||
|
for (EquipmentItem item : snapshot.equipment)
|
||||||
|
{
|
||||||
|
html.append("<tr><td>").append(escape(item.slot)).append("</td><td><img src=\"").append(item.icon)
|
||||||
|
.append("\" width=\"24\" height=\"24\"></td><td>").append(escape(item.name))
|
||||||
|
.append("</td><td>").append(price(item.gePrice)).append("</td><td>").append(price(item.haPrice)).append("</td></tr>");
|
||||||
|
}
|
||||||
|
html.append("<tr><th colspan=\"3\">Known total</th><th>").append(price(snapshot.geTotal))
|
||||||
|
.append("</th><th>").append(price(snapshot.haTotal)).append("</th></tr></table>");
|
||||||
|
}
|
||||||
|
html.append("<p>Only visible items can be identified; rings and ammunition are not shown on the player model.</p>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int combatLevelFrom(HiscoreResult hiscores)
|
||||||
|
{
|
||||||
|
if (hiscores == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int attack = level(hiscores, HiscoreSkill.ATTACK);
|
||||||
|
int strength = level(hiscores, HiscoreSkill.STRENGTH);
|
||||||
|
int defence = level(hiscores, HiscoreSkill.DEFENCE);
|
||||||
|
int hitpoints = level(hiscores, HiscoreSkill.HITPOINTS);
|
||||||
|
int prayer = level(hiscores, HiscoreSkill.PRAYER);
|
||||||
|
int ranged = level(hiscores, HiscoreSkill.RANGED);
|
||||||
|
int magic = level(hiscores, HiscoreSkill.MAGIC);
|
||||||
|
return combatLevel(attack, strength, defence, hitpoints, prayer, ranged, magic);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int combatLevel(int attack, int strength, int defence, int hitpoints, int prayer, int ranged, int magic)
|
||||||
|
{
|
||||||
|
if (attack <= 0 || strength <= 0 || defence <= 0 || hitpoints <= 0 || prayer <= 0 || ranged <= 0 || magic <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double base = 0.25 * (defence + hitpoints + Math.floor(prayer / 2.0));
|
||||||
|
double melee = 0.325 * (attack + strength);
|
||||||
|
double rangedCombat = 0.325 * Math.floor(ranged * 1.5);
|
||||||
|
double magicCombat = 0.325 * Math.floor(magic * 1.5);
|
||||||
|
return (int) Math.floor(base + Math.max(melee, Math.max(rangedCombat, magicCombat)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int level(HiscoreResult hiscores, HiscoreSkill skill)
|
||||||
|
{
|
||||||
|
Skill value = hiscores.getSkill(skill);
|
||||||
|
return value == null || value.getLevel() < 0 ? 0 : value.getLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long combatLevelDelta(HiscoreResult hiscores, PlayerHiscoreHistory.Comparison comparison)
|
||||||
|
{
|
||||||
|
if (hiscores == null || comparison == null || !comparison.hasBaseline())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int attack = baselineLevel(hiscores, comparison, HiscoreSkill.ATTACK);
|
||||||
|
int strength = baselineLevel(hiscores, comparison, HiscoreSkill.STRENGTH);
|
||||||
|
int defence = baselineLevel(hiscores, comparison, HiscoreSkill.DEFENCE);
|
||||||
|
int hitpoints = baselineLevel(hiscores, comparison, HiscoreSkill.HITPOINTS);
|
||||||
|
int prayer = baselineLevel(hiscores, comparison, HiscoreSkill.PRAYER);
|
||||||
|
int ranged = baselineLevel(hiscores, comparison, HiscoreSkill.RANGED);
|
||||||
|
int magic = baselineLevel(hiscores, comparison, HiscoreSkill.MAGIC);
|
||||||
|
int baseline = combatLevel(attack, strength, defence, hitpoints, prayer, ranged, magic);
|
||||||
|
int current = combatLevelFrom(hiscores);
|
||||||
|
return baseline == 0 || current == 0 ? null : (long) current - baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int baselineLevel(HiscoreResult hiscores, PlayerHiscoreHistory.Comparison comparison, HiscoreSkill skill)
|
||||||
|
{
|
||||||
|
Long delta = comparison.levelDelta(skill);
|
||||||
|
return delta == null ? 0 : level(hiscores, skill) - delta.intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendBossKills(StringBuilder html, HiscoreResult hiscores, PlayerHiscoreHistory.Comparison comparison)
|
||||||
|
{
|
||||||
|
List<BossKill> kills = bossKills(hiscores);
|
||||||
|
html.append("<h2>Boss kills</h2>");
|
||||||
|
if (kills.isEmpty())
|
||||||
|
{
|
||||||
|
html.append("<p>No boss kills listed in public hiscores.</p>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.append("<table><tr><th colspan=\"2\">Boss</th><th>Kills</th></tr>");
|
||||||
|
for (BossKill kill : kills)
|
||||||
|
{
|
||||||
|
html.append("<tr><td><img src=\"").append(bossIcon(kill.skill)).append("\" width=\"20\" height=\"20\"></td><td>")
|
||||||
|
.append(escape(kill.skill.getName())).append("</td><td>").append(number(kill.kills))
|
||||||
|
.append(deltaMarkup(comparison == null ? null : comparison.levelDelta(kill.skill))).append("</td></tr>");
|
||||||
|
}
|
||||||
|
html.append("</table>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String comparisonLabel(PlayerHiscoreHistory.Comparison comparison, PlayerHiscoreHistory.Range range)
|
||||||
|
{
|
||||||
|
PlayerHiscoreHistory.Range selected = comparison == null ? range : comparison.range;
|
||||||
|
String label = selected == null ? PlayerHiscoreHistory.Range.LAST_VISIT.label : selected.label;
|
||||||
|
return comparison != null && comparison.hasBaseline()
|
||||||
|
? label + " since " + FETCHED_AT.format(Instant.ofEpochMilli(comparison.fromTime))
|
||||||
|
: label + (selected == PlayerHiscoreHistory.Range.LAST_VISIT
|
||||||
|
? " (no earlier snapshot yet)"
|
||||||
|
: " (no snapshot in this range)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String deltaMarkup(Long delta)
|
||||||
|
{
|
||||||
|
if (delta == null || delta == 0)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String color = delta > 0 ? "#55D16C" : "#FF7070";
|
||||||
|
return " <font color=\"" + color + "\">(" + (delta > 0 ? "+" : "") + number(delta) + ")</font>";
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BossKill> getBossKills(HiscoreResult hiscores)
|
||||||
|
{
|
||||||
|
return bossKills(hiscores);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<BossKill> bossKills(HiscoreResult hiscores)
|
||||||
|
{
|
||||||
|
if (hiscores == null)
|
||||||
|
{
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<BossKill> kills = new ArrayList<>();
|
||||||
|
for (HiscoreSkill skill : HiscoreSkill.values())
|
||||||
|
{
|
||||||
|
if (skill.getType() != HiscoreSkillType.BOSS)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Skill value = hiscores.getSkill(skill);
|
||||||
|
if (value != null && value.getLevel() > 0)
|
||||||
|
{
|
||||||
|
kills.add(new BossKill(skill, value.getLevel()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kills.sort(Comparator.comparingInt((BossKill kill) -> kill.kills).reversed()
|
||||||
|
.thenComparing(kill -> kill.skill.getName(), String.CASE_INSENSITIVE_ORDER));
|
||||||
|
return kills;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String bossIcon(HiscoreSkill skill)
|
||||||
|
{
|
||||||
|
return LOCAL_IMAGE_BASE + "boss/" + skill.name().toLowerCase(Locale.ROOT) + ".png";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String itemIcon(int itemId)
|
||||||
|
{
|
||||||
|
return LOCAL_IMAGE_BASE + "item/" + itemId + ".png";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String skillIcon(HiscoreSkill skill)
|
||||||
|
{
|
||||||
|
return LOCAL_IMAGE_BASE + "skill/" + skill.name().toLowerCase(Locale.ROOT) + ".png";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String liveHealth(Snapshot snapshot, int hitpointsLevel)
|
||||||
|
{
|
||||||
|
if (snapshot.healthRatio < 0 || snapshot.healthScale <= 0)
|
||||||
|
{
|
||||||
|
return "Unavailable";
|
||||||
|
}
|
||||||
|
if (hitpointsLevel <= 0)
|
||||||
|
{
|
||||||
|
return number(snapshot.healthRatio) + " / " + number(snapshot.healthScale) + " health-bar ratio";
|
||||||
|
}
|
||||||
|
long estimate = Math.round((double) hitpointsLevel * snapshot.healthRatio / snapshot.healthScale);
|
||||||
|
estimate = Math.max(0, Math.min(hitpointsLevel, estimate));
|
||||||
|
return "~" + number(estimate) + " / " + number(hitpointsLevel) + " HP (health-bar estimate)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String slotName(KitType slot)
|
||||||
|
{
|
||||||
|
int index = slot.getIndex();
|
||||||
|
return index >= 0 && index < SLOT_NAMES.length ? SLOT_NAMES[index] : slot.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String price(long value)
|
||||||
|
{
|
||||||
|
return value <= 0 ? "—" : number(value) + " gp";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String number(long value)
|
||||||
|
{
|
||||||
|
return NumberFormat.getIntegerInstance(Locale.US).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escape(String value)
|
||||||
|
{
|
||||||
|
return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Snapshot
|
||||||
|
{
|
||||||
|
public final String name;
|
||||||
|
public final int combatLevel;
|
||||||
|
public final int healthRatio;
|
||||||
|
public final int healthScale;
|
||||||
|
public final List<EquipmentItem> equipment;
|
||||||
|
public final long geTotal;
|
||||||
|
public final long haTotal;
|
||||||
|
public final Map<String, BufferedImage> images;
|
||||||
|
public final boolean publicSearchOnly;
|
||||||
|
|
||||||
|
private Snapshot(String name, int combatLevel, int healthRatio, int healthScale, List<EquipmentItem> equipment,
|
||||||
|
Map<String, BufferedImage> images, boolean publicSearchOnly)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.combatLevel = combatLevel;
|
||||||
|
this.healthRatio = healthRatio;
|
||||||
|
this.healthScale = healthScale;
|
||||||
|
this.equipment = Collections.unmodifiableList(new ArrayList<>(equipment));
|
||||||
|
this.geTotal = equipment.stream().mapToLong(item -> item.gePrice).sum();
|
||||||
|
this.haTotal = equipment.stream().mapToLong(item -> item.haPrice).sum();
|
||||||
|
this.images = images;
|
||||||
|
this.publicSearchOnly = publicSearchOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class EquipmentItem
|
||||||
|
{
|
||||||
|
public final String slot;
|
||||||
|
public final String name;
|
||||||
|
public final long gePrice;
|
||||||
|
public final long haPrice;
|
||||||
|
public final String icon;
|
||||||
|
|
||||||
|
private EquipmentItem(String slot, String name, long gePrice, long haPrice, String icon)
|
||||||
|
{
|
||||||
|
this.slot = slot;
|
||||||
|
this.name = name;
|
||||||
|
this.gePrice = gePrice;
|
||||||
|
this.haPrice = haPrice;
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class BossKill
|
||||||
|
{
|
||||||
|
public final HiscoreSkill skill;
|
||||||
|
public final int kills;
|
||||||
|
|
||||||
|
private BossKill(HiscoreSkill skill, int kills)
|
||||||
|
{
|
||||||
|
this.skill = skill;
|
||||||
|
this.kills = kills;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
package com.example.essentialsplus.player;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.client.RuneLite;
|
||||||
|
import net.runelite.client.hiscore.HiscoreResult;
|
||||||
|
import net.runelite.client.hiscore.HiscoreSkill;
|
||||||
|
import net.runelite.client.hiscore.Skill;
|
||||||
|
|
||||||
|
/** Stores recent public hiscore snapshots, expiring profiles one week after their last view. */
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class PlayerHiscoreHistory
|
||||||
|
{
|
||||||
|
private static final long DAY_MILLIS = 24L * 60L * 60L * 1000L;
|
||||||
|
private static final long RETENTION_MILLIS = 7L * DAY_MILLIS;
|
||||||
|
private static final int MAX_SAMPLES_PER_PLAYER = 256;
|
||||||
|
private static final Path FILE = RuneLite.RUNELITE_DIR.toPath()
|
||||||
|
.resolve("plugin-data")
|
||||||
|
.resolve("essentialsplus")
|
||||||
|
.resolve("player-hiscores.json");
|
||||||
|
private static final Type STORE_TYPE = new TypeToken<Map<String, PlayerRecord>>()
|
||||||
|
{
|
||||||
|
}.getType();
|
||||||
|
|
||||||
|
public enum Range
|
||||||
|
{
|
||||||
|
LAST_VISIT("Since last", 0),
|
||||||
|
DAILY("Daily", DAY_MILLIS),
|
||||||
|
WEEKLY("Weekly", RETENTION_MILLIS);
|
||||||
|
|
||||||
|
public final String label;
|
||||||
|
private final long duration;
|
||||||
|
|
||||||
|
Range(String label, long duration)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Comparison
|
||||||
|
{
|
||||||
|
public final Range range;
|
||||||
|
public final long fromTime;
|
||||||
|
public final long toTime;
|
||||||
|
public final Map<String, Long> levelDeltas;
|
||||||
|
public final Map<String, Long> experienceDeltas;
|
||||||
|
|
||||||
|
private Comparison(Range range, long fromTime, long toTime, Map<String, Long> levelDeltas,
|
||||||
|
Map<String, Long> experienceDeltas)
|
||||||
|
{
|
||||||
|
this.range = range;
|
||||||
|
this.fromTime = fromTime;
|
||||||
|
this.toTime = toTime;
|
||||||
|
this.levelDeltas = Collections.unmodifiableMap(levelDeltas);
|
||||||
|
this.experienceDeltas = Collections.unmodifiableMap(experienceDeltas);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasBaseline()
|
||||||
|
{
|
||||||
|
return fromTime > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long levelDelta(HiscoreSkill skill)
|
||||||
|
{
|
||||||
|
return levelDeltas.get(skill.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long experienceDelta(HiscoreSkill skill)
|
||||||
|
{
|
||||||
|
return experienceDeltas.get(skill.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Gson gson;
|
||||||
|
private final Map<String, PlayerRecord> players = new HashMap<>();
|
||||||
|
private boolean loaded;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PlayerHiscoreHistory(Gson gson)
|
||||||
|
{
|
||||||
|
this.gson = gson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void load()
|
||||||
|
{
|
||||||
|
if (loaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loaded = true;
|
||||||
|
if (Files.exists(FILE))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Map<String, PlayerRecord> stored = gson.fromJson(Files.readString(FILE, StandardCharsets.UTF_8), STORE_TYPE);
|
||||||
|
if (stored != null)
|
||||||
|
{
|
||||||
|
for (Map.Entry<String, PlayerRecord> entry : stored.entrySet())
|
||||||
|
{
|
||||||
|
if (entry.getKey() != null && entry.getValue() != null)
|
||||||
|
{
|
||||||
|
entry.getValue().normalize();
|
||||||
|
players.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("Player hiscores: could not read history", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prune(System.currentTimeMillis()))
|
||||||
|
{
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Marks the profile active; it will expire one week after its most recent view. */
|
||||||
|
public synchronized void viewed(String playerName, long now)
|
||||||
|
{
|
||||||
|
load();
|
||||||
|
prune(now);
|
||||||
|
String key = key(playerName);
|
||||||
|
if (key.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayerRecord player = players.computeIfAbsent(key, ignored -> new PlayerRecord());
|
||||||
|
player.name = playerName;
|
||||||
|
player.lastViewedAt = now;
|
||||||
|
pruneSamples(player, now);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes profiles and snapshots that have passed their seven-day retention window. */
|
||||||
|
public synchronized void pruneExpired(long now)
|
||||||
|
{
|
||||||
|
load();
|
||||||
|
if (prune(now))
|
||||||
|
{
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Saves one fetched result and returns a snapshot delta for the selected period. */
|
||||||
|
public synchronized Comparison record(String playerName, HiscoreResult result, long fetchedAt, Range range)
|
||||||
|
{
|
||||||
|
load();
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return empty(range);
|
||||||
|
}
|
||||||
|
prune(fetchedAt);
|
||||||
|
String key = key(playerName);
|
||||||
|
if (key.isEmpty())
|
||||||
|
{
|
||||||
|
return empty(range);
|
||||||
|
}
|
||||||
|
PlayerRecord player = players.computeIfAbsent(key, ignored -> new PlayerRecord());
|
||||||
|
player.name = playerName;
|
||||||
|
player.lastViewedAt = fetchedAt;
|
||||||
|
pruneSamples(player, fetchedAt);
|
||||||
|
|
||||||
|
Sample sample = new Sample();
|
||||||
|
sample.fetchedAt = fetchedAt;
|
||||||
|
for (Map.Entry<HiscoreSkill, Skill> entry : result.getSkills().entrySet())
|
||||||
|
{
|
||||||
|
Skill value = entry.getValue();
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value.getLevel() >= 0)
|
||||||
|
{
|
||||||
|
sample.levels.put(entry.getKey().name(), (long) value.getLevel());
|
||||||
|
}
|
||||||
|
if (value.getExperience() >= 0)
|
||||||
|
{
|
||||||
|
sample.experience.put(entry.getKey().name(), value.getExperience());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.samples.add(sample);
|
||||||
|
player.samples.sort(Comparator.comparingLong(s -> s.fetchedAt));
|
||||||
|
while (player.samples.size() > MAX_SAMPLES_PER_PLAYER)
|
||||||
|
{
|
||||||
|
player.samples.remove(0);
|
||||||
|
}
|
||||||
|
Comparison comparison = compare(player, range == null ? Range.LAST_VISIT : range);
|
||||||
|
save();
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compares the latest stored sample with the closest available baseline for the requested period. */
|
||||||
|
public synchronized Comparison comparison(String playerName, Range range, long now)
|
||||||
|
{
|
||||||
|
load();
|
||||||
|
if (prune(now))
|
||||||
|
{
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
PlayerRecord player = players.get(key(playerName));
|
||||||
|
return player == null ? empty(range) : compare(player, range == null ? Range.LAST_VISIT : range);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparison compare(PlayerRecord player, Range range)
|
||||||
|
{
|
||||||
|
List<Sample> samples = player.samples;
|
||||||
|
if (samples.size() < 2)
|
||||||
|
{
|
||||||
|
return empty(range);
|
||||||
|
}
|
||||||
|
Sample current = samples.get(samples.size() - 1);
|
||||||
|
int baselineIndex = -1;
|
||||||
|
if (range == Range.LAST_VISIT)
|
||||||
|
{
|
||||||
|
baselineIndex = samples.size() - 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long threshold = current.fetchedAt - range.duration;
|
||||||
|
for (int i = 0; i < samples.size() - 1; i++)
|
||||||
|
{
|
||||||
|
if (samples.get(i).fetchedAt >= threshold)
|
||||||
|
{
|
||||||
|
baselineIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (baselineIndex < 0)
|
||||||
|
{
|
||||||
|
return empty(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sample baseline = samples.get(baselineIndex);
|
||||||
|
return new Comparison(range, baseline.fetchedAt, current.fetchedAt,
|
||||||
|
differences(baseline.levels, current.levels), differences(baseline.experience, current.experience));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Long> differences(Map<String, Long> baseline, Map<String, Long> current)
|
||||||
|
{
|
||||||
|
Map<String, Long> deltas = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, Long> entry : current.entrySet())
|
||||||
|
{
|
||||||
|
Long previous = baseline.get(entry.getKey());
|
||||||
|
if (previous != null)
|
||||||
|
{
|
||||||
|
deltas.put(entry.getKey(), entry.getValue() - previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deltas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Comparison empty(Range range)
|
||||||
|
{
|
||||||
|
return new Comparison(range == null ? Range.LAST_VISIT : range, 0, 0, new LinkedHashMap<>(), new LinkedHashMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean prune(long now)
|
||||||
|
{
|
||||||
|
long cutoff = now - RETENTION_MILLIS;
|
||||||
|
boolean changed = players.entrySet().removeIf(e -> e.getValue().lastViewedAt < cutoff);
|
||||||
|
for (PlayerRecord player : players.values())
|
||||||
|
{
|
||||||
|
changed |= pruneSamples(player, now);
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean pruneSamples(PlayerRecord player, long now)
|
||||||
|
{
|
||||||
|
return player.samples.removeIf(sample -> sample.fetchedAt < now - RETENTION_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String key(String playerName)
|
||||||
|
{
|
||||||
|
return playerName == null ? "" : net.runelite.client.util.Text.removeTags(playerName).trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.createDirectories(FILE.getParent());
|
||||||
|
Path temp = FILE.resolveSibling(FILE.getFileName() + ".tmp");
|
||||||
|
Files.writeString(temp, gson.toJson(players), StandardCharsets.UTF_8);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.move(temp, FILE, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
catch (AtomicMoveNotSupportedException e)
|
||||||
|
{
|
||||||
|
Files.move(temp, FILE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
log.warn("Player hiscores: could not save history", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PlayerRecord
|
||||||
|
{
|
||||||
|
String name;
|
||||||
|
long lastViewedAt;
|
||||||
|
List<Sample> samples = new ArrayList<>();
|
||||||
|
|
||||||
|
void normalize()
|
||||||
|
{
|
||||||
|
if (samples == null)
|
||||||
|
{
|
||||||
|
samples = new ArrayList<>();
|
||||||
|
}
|
||||||
|
samples.removeIf(s -> s == null);
|
||||||
|
for (Sample sample : samples)
|
||||||
|
{
|
||||||
|
sample.normalize();
|
||||||
|
}
|
||||||
|
samples.sort(Comparator.comparingLong(s -> s.fetchedAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Sample
|
||||||
|
{
|
||||||
|
long fetchedAt;
|
||||||
|
Map<String, Long> levels = new HashMap<>();
|
||||||
|
Map<String, Long> experience = new HashMap<>();
|
||||||
|
|
||||||
|
void normalize()
|
||||||
|
{
|
||||||
|
if (levels == null)
|
||||||
|
{
|
||||||
|
levels = new HashMap<>();
|
||||||
|
}
|
||||||
|
if (experience == null)
|
||||||
|
{
|
||||||
|
experience = new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,12 +164,12 @@ public class SkillHistory
|
|||||||
|
|
||||||
public synchronized void reset(TimeRange range)
|
public synchronized void reset(TimeRange range)
|
||||||
{
|
{
|
||||||
if (range == TimeRange.SESSION)
|
if (range != null && range.isSession())
|
||||||
{
|
{
|
||||||
resetSessionAt = System.currentTimeMillis();
|
resetSessionAt = System.currentTimeMillis();
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
else if (range == TimeRange.CURRENT)
|
else if (range != null && range.isCurrent())
|
||||||
{
|
{
|
||||||
resetNowAt = System.currentTimeMillis();
|
resetNowAt = System.currentTimeMillis();
|
||||||
dirty = true;
|
dirty = true;
|
||||||
@@ -220,7 +220,7 @@ public class SkillHistory
|
|||||||
long streak = currentStart(i, now);
|
long streak = currentStart(i, now);
|
||||||
out[i] = streak == Long.MAX_VALUE ? Long.MAX_VALUE : Math.max(streak, resetNowAt);
|
out[i] = streak == Long.MAX_VALUE ? Long.MAX_VALUE : Math.max(streak, resetNowAt);
|
||||||
}
|
}
|
||||||
else if (filter.getRange() == TimeRange.SESSION)
|
else if (filter.getRange() != null && filter.getRange().isSession())
|
||||||
{
|
{
|
||||||
out[i] = Math.max(filter.start(), resetSessionAt);
|
out[i] = Math.max(filter.start(), resetSessionAt);
|
||||||
}
|
}
|
||||||
@@ -242,7 +242,7 @@ public class SkillHistory
|
|||||||
long base = start == Long.MAX_VALUE ? now : start;
|
long base = start == Long.MAX_VALUE ? now : start;
|
||||||
return Math.max(base, resetNowAt);
|
return Math.max(base, resetNowAt);
|
||||||
}
|
}
|
||||||
if (filter.getRange() == TimeRange.SESSION)
|
if (filter.getRange() != null && filter.getRange().isSession())
|
||||||
{
|
{
|
||||||
return Math.max(filter.start(), resetSessionAt);
|
return Math.max(filter.start(), resetSessionAt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,19 +12,29 @@ import com.example.essentialsplus.wiki.ui.WikiCard;
|
|||||||
import com.example.essentialsplus.wiki.ui.WikiPanel;
|
import com.example.essentialsplus.wiki.ui.WikiPanel;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.CardLayout;
|
import java.awt.CardLayout;
|
||||||
|
import java.awt.Component;
|
||||||
import java.awt.Cursor;
|
import java.awt.Cursor;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
import java.awt.GridLayout;
|
import java.awt.GridLayout;
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.Box;
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
import javax.swing.JCheckBoxMenuItem;
|
import javax.swing.JCheckBoxMenuItem;
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
import javax.swing.JMenu;
|
import javax.swing.JMenu;
|
||||||
import javax.swing.JMenuItem;
|
import javax.swing.JMenuItem;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPopupMenu;
|
import javax.swing.JPopupMenu;
|
||||||
import javax.swing.JRadioButtonMenuItem;
|
import javax.swing.JRadioButtonMenuItem;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.ScrollPaneConstants;
|
import javax.swing.ScrollPaneConstants;
|
||||||
|
import javax.swing.JTextArea;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -55,11 +65,13 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
private static final String VIEW_WIKI = "WIKI";
|
private static final String VIEW_WIKI = "WIKI";
|
||||||
private static final String VIEW_PROGRESS = "PROGRESS";
|
private static final String VIEW_PROGRESS = "PROGRESS";
|
||||||
private static final String VIEW_CURRENCY = "CURRENCY";
|
private static final String VIEW_CURRENCY = "CURRENCY";
|
||||||
|
private static final String VIEW_EQUIPMENT = "EQUIPMENT";
|
||||||
|
|
||||||
private final CardLayout cardLayout = new CardLayout();
|
private final CardLayout cardLayout = new CardLayout();
|
||||||
private final JPanel container = new JPanel(cardLayout);
|
private final JPanel container = new JPanel(cardLayout);
|
||||||
|
|
||||||
private final EssentialsPlusPlugin plugin;
|
private final EssentialsPlusPlugin plugin;
|
||||||
|
private Runnable refreshEquipmentProfiles;
|
||||||
private final OverviewPanel overviewPanel;
|
private final OverviewPanel overviewPanel;
|
||||||
private final WealthPanel wealthPanel;
|
private final WealthPanel wealthPanel;
|
||||||
private final LootTrackerPanel lootTrackerPanel;
|
private final LootTrackerPanel lootTrackerPanel;
|
||||||
@@ -109,15 +121,16 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
container.add(wikiPanel, VIEW_WIKI);
|
container.add(wikiPanel, VIEW_WIKI);
|
||||||
container.add(progressPanel, VIEW_PROGRESS);
|
container.add(progressPanel, VIEW_PROGRESS);
|
||||||
container.add(currencyPanel, VIEW_CURRENCY);
|
container.add(currencyPanel, VIEW_CURRENCY);
|
||||||
|
container.add(createEquipmentPanel(), VIEW_EQUIPMENT);
|
||||||
|
|
||||||
add(container, BorderLayout.CENTER);
|
add(container, BorderLayout.CENTER);
|
||||||
rangeBar = Segmented.ranges(plugin.getTimeFilter().getRange(), plugin.getTimeFilter()::set);
|
rangeBar = Segmented.ranges(plugin.getTimeFilter(), plugin.getTimeFilter()::set);
|
||||||
add(buildFooter(), BorderLayout.SOUTH);
|
add(buildFooter(), BorderLayout.SOUTH);
|
||||||
|
|
||||||
TimeFilter filter = plugin.getTimeFilter();
|
TimeFilter filter = plugin.getTimeFilter();
|
||||||
rangeListener = () -> SwingUtilities.invokeLater(() ->
|
rangeListener = () -> SwingUtilities.invokeLater(() ->
|
||||||
{
|
{
|
||||||
rangeBar.setSelected(filter.getRange().ordinal());
|
rangeBar.setSelected(filter.getSlotIndex(filter.getRange()));
|
||||||
overviewPanel.refreshSkills();
|
overviewPanel.refreshSkills();
|
||||||
currencyPanel.refresh();
|
currencyPanel.refresh();
|
||||||
});
|
});
|
||||||
@@ -209,6 +222,282 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
currencyPanel.refresh();
|
currencyPanel.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showEquipment()
|
||||||
|
{
|
||||||
|
cardLayout.show(container, VIEW_EQUIPMENT);
|
||||||
|
if (refreshEquipmentProfiles != null)
|
||||||
|
{
|
||||||
|
refreshEquipmentProfiles.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshEquipmentProfilesLater()
|
||||||
|
{
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
if (refreshEquipmentProfiles != null)
|
||||||
|
{
|
||||||
|
refreshEquipmentProfiles.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createEquipmentPanel()
|
||||||
|
{
|
||||||
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
panel.setBackground(Ui.BG);
|
||||||
|
|
||||||
|
JPanel header = new JPanel(new BorderLayout(8, 0));
|
||||||
|
header.setOpaque(false);
|
||||||
|
header.setBorder(BorderFactory.createEmptyBorder(12, 12, 10, 12));
|
||||||
|
JPanel titleBlock = new JPanel();
|
||||||
|
titleBlock.setOpaque(false);
|
||||||
|
titleBlock.setLayout(new BoxLayout(titleBlock, BoxLayout.Y_AXIS));
|
||||||
|
JLabel label = new JLabel("Equipment");
|
||||||
|
label.setFont(FontManager.getRunescapeBoldFont().deriveFont(16f));
|
||||||
|
label.setForeground(Ui.TEXT);
|
||||||
|
JLabel subtitle = new JLabel("Saved bank loadouts");
|
||||||
|
subtitle.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
subtitle.setForeground(Ui.MUTED);
|
||||||
|
titleBlock.add(label);
|
||||||
|
titleBlock.add(Box.createVerticalStrut(2));
|
||||||
|
titleBlock.add(subtitle);
|
||||||
|
header.add(titleBlock, BorderLayout.CENTER);
|
||||||
|
header.add(createSmallButton("Back", this::showOverview), BorderLayout.EAST);
|
||||||
|
panel.add(header, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
JPanel controls = new JPanel();
|
||||||
|
controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS));
|
||||||
|
controls.setOpaque(false);
|
||||||
|
controls.setBorder(BorderFactory.createEmptyBorder(2, 12, 12, 12));
|
||||||
|
|
||||||
|
JPanel loadouts = new JPanel();
|
||||||
|
loadouts.setLayout(new BoxLayout(loadouts, BoxLayout.Y_AXIS));
|
||||||
|
loadouts.setBackground(Ui.CARD);
|
||||||
|
loadouts.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(0, 3, 0, 0, Ui.ACCENT),
|
||||||
|
BorderFactory.createEmptyBorder(10, 10, 10, 10)));
|
||||||
|
loadouts.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
|
||||||
|
JPanel savedTitleRow = new JPanel(new BorderLayout(6, 0));
|
||||||
|
savedTitleRow.setOpaque(false);
|
||||||
|
JLabel savedLabel = new JLabel("Saved loadouts");
|
||||||
|
savedLabel.setFont(FontManager.getRunescapeBoldFont().deriveFont(12f));
|
||||||
|
savedLabel.setForeground(Ui.TEXT);
|
||||||
|
JLabel layoutCount = new JLabel("0 saved");
|
||||||
|
layoutCount.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
layoutCount.setForeground(Ui.MUTED);
|
||||||
|
savedTitleRow.add(savedLabel, BorderLayout.WEST);
|
||||||
|
savedTitleRow.add(layoutCount, BorderLayout.EAST);
|
||||||
|
savedTitleRow.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
savedTitleRow.setMaximumSize(new Dimension(Integer.MAX_VALUE, savedTitleRow.getPreferredSize().height));
|
||||||
|
loadouts.add(savedTitleRow);
|
||||||
|
loadouts.add(Box.createVerticalStrut(4));
|
||||||
|
|
||||||
|
JLabel savedDescription = new JLabel("Equipment and inventory snapshots");
|
||||||
|
savedDescription.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
savedDescription.setForeground(Ui.MUTED);
|
||||||
|
savedDescription.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
loadouts.add(savedDescription);
|
||||||
|
loadouts.add(Box.createVerticalStrut(10));
|
||||||
|
|
||||||
|
JComboBox<String> profiles = new JComboBox<>();
|
||||||
|
profiles.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
profiles.setForeground(Ui.TEXT);
|
||||||
|
profiles.setBackground(Ui.CARD_HOVER);
|
||||||
|
profiles.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createLineBorder(Ui.FAINT),
|
||||||
|
BorderFactory.createEmptyBorder(2, 4, 2, 4)));
|
||||||
|
profiles.setMaximumRowCount(10);
|
||||||
|
profiles.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
profiles.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
|
||||||
|
loadouts.add(profiles);
|
||||||
|
loadouts.add(Box.createVerticalStrut(7));
|
||||||
|
|
||||||
|
JLabel emptyHint = new JLabel("<html>No loadouts yet.<br>Save your current setup to start.</html>");
|
||||||
|
emptyHint.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
emptyHint.setForeground(Ui.MUTED);
|
||||||
|
emptyHint.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
loadouts.add(emptyHint);
|
||||||
|
loadouts.add(Box.createVerticalStrut(7));
|
||||||
|
|
||||||
|
JTextArea status = new JTextArea("Bank contents stay unchanged.");
|
||||||
|
status.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
status.setForeground(Ui.MUTED);
|
||||||
|
status.setOpaque(false);
|
||||||
|
status.setEditable(false);
|
||||||
|
status.setFocusable(false);
|
||||||
|
status.setLineWrap(true);
|
||||||
|
status.setWrapStyleWord(true);
|
||||||
|
status.setRows(2);
|
||||||
|
status.setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
|
||||||
|
status.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
status.setMaximumSize(new Dimension(Integer.MAX_VALUE, status.getPreferredSize().height));
|
||||||
|
|
||||||
|
Runnable refreshProfiles = () ->
|
||||||
|
{
|
||||||
|
String selected = (String) profiles.getSelectedItem();
|
||||||
|
List<String> names = plugin.getSavedEquipmentLayoutNames();
|
||||||
|
profiles.removeAllItems();
|
||||||
|
for (String savedName : names)
|
||||||
|
{
|
||||||
|
profiles.addItem(savedName);
|
||||||
|
}
|
||||||
|
if (selected != null && names.contains(selected))
|
||||||
|
{
|
||||||
|
profiles.setSelectedItem(selected);
|
||||||
|
}
|
||||||
|
else if (profiles.getItemCount() > 0)
|
||||||
|
{
|
||||||
|
profiles.setSelectedIndex(0);
|
||||||
|
}
|
||||||
|
layoutCount.setText(names.size() == 1 ? "1 saved" : names.size() + " saved");
|
||||||
|
emptyHint.setVisible(names.isEmpty());
|
||||||
|
profiles.setEnabled(profiles.getItemCount() > 0);
|
||||||
|
Dimension loadoutsSize = loadouts.getPreferredSize();
|
||||||
|
loadouts.setMaximumSize(new Dimension(Integer.MAX_VALUE, loadoutsSize.height));
|
||||||
|
controls.revalidate();
|
||||||
|
controls.repaint();
|
||||||
|
};
|
||||||
|
refreshEquipmentProfiles = refreshProfiles;
|
||||||
|
|
||||||
|
JButton saveLayout = createEquipmentActionButton("Save equipment + inventory", () ->
|
||||||
|
{
|
||||||
|
String name = JOptionPane.showInputDialog(panel, "Loadout name:", "Save loadout", JOptionPane.PLAIN_MESSAGE);
|
||||||
|
if (name == null || name.trim().isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (plugin.hasSavedEquipmentLayout(name)
|
||||||
|
&& JOptionPane.showConfirmDialog(panel, "Replace the saved layout '" + name.trim() + "'?", "Replace loadout", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.saveEquipmentAndInventory(name, message ->
|
||||||
|
{
|
||||||
|
status.setText(message);
|
||||||
|
refreshProfiles.run();
|
||||||
|
profiles.setSelectedItem(name.trim());
|
||||||
|
});
|
||||||
|
}, false, false);
|
||||||
|
loadouts.add(saveLayout);
|
||||||
|
loadouts.add(Box.createVerticalStrut(10));
|
||||||
|
|
||||||
|
JButton showLayout = createEquipmentActionButton("Show in bank", () ->
|
||||||
|
{
|
||||||
|
String selected = (String) profiles.getSelectedItem();
|
||||||
|
if (selected != null)
|
||||||
|
{
|
||||||
|
plugin.showSavedEquipmentLayout(selected, status::setText);
|
||||||
|
}
|
||||||
|
}, true, false);
|
||||||
|
loadouts.add(showLayout);
|
||||||
|
loadouts.add(Box.createVerticalStrut(6));
|
||||||
|
|
||||||
|
JButton renameLayout = createEquipmentActionButton("Rename", () ->
|
||||||
|
{
|
||||||
|
String selected = (String) profiles.getSelectedItem();
|
||||||
|
if (selected == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String newName = JOptionPane.showInputDialog(panel, "New layout name:", selected);
|
||||||
|
if (newName == null || newName.trim().isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.renameSavedEquipmentLayout(selected, newName, message ->
|
||||||
|
{
|
||||||
|
status.setText(message);
|
||||||
|
refreshProfiles.run();
|
||||||
|
if (message.startsWith("Renamed "))
|
||||||
|
{
|
||||||
|
profiles.setSelectedItem(newName.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, false, false);
|
||||||
|
JButton deleteLayout = createEquipmentActionButton("Delete", () ->
|
||||||
|
{
|
||||||
|
String selected = (String) profiles.getSelectedItem();
|
||||||
|
if (selected == null || JOptionPane.showConfirmDialog(panel,
|
||||||
|
"Delete the saved layout '" + selected + "'?", "Delete layout", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.deleteSavedEquipmentLayout(selected, message ->
|
||||||
|
{
|
||||||
|
status.setText(message);
|
||||||
|
refreshProfiles.run();
|
||||||
|
});
|
||||||
|
}, false, true);
|
||||||
|
|
||||||
|
JPanel savedActions = new JPanel(new GridLayout(1, 2, 6, 0));
|
||||||
|
savedActions.setOpaque(false);
|
||||||
|
savedActions.add(renameLayout);
|
||||||
|
savedActions.add(deleteLayout);
|
||||||
|
savedActions.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
savedActions.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
|
||||||
|
loadouts.add(savedActions);
|
||||||
|
loadouts.add(Box.createVerticalStrut(6));
|
||||||
|
loadouts.add(status);
|
||||||
|
controls.add(loadouts);
|
||||||
|
controls.add(Box.createVerticalStrut(12));
|
||||||
|
|
||||||
|
JPanel bankDisplay = new JPanel();
|
||||||
|
bankDisplay.setLayout(new BoxLayout(bankDisplay, BoxLayout.Y_AXIS));
|
||||||
|
bankDisplay.setOpaque(false);
|
||||||
|
bankDisplay.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createMatteBorder(1, 0, 0, 0, Ui.GRID),
|
||||||
|
BorderFactory.createEmptyBorder(10, 0, 0, 0)));
|
||||||
|
bankDisplay.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
JLabel bankTitle = new JLabel("Bank display");
|
||||||
|
bankTitle.setFont(FontManager.getRunescapeBoldFont().deriveFont(12f));
|
||||||
|
bankTitle.setForeground(Ui.TEXT);
|
||||||
|
JLabel bankDescription = new JLabel("Restore every bank item.");
|
||||||
|
bankDescription.setFont(FontManager.getRunescapeSmallFont());
|
||||||
|
bankDescription.setForeground(Ui.MUTED);
|
||||||
|
bankDescription.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
bankDisplay.add(bankTitle);
|
||||||
|
bankDisplay.add(Box.createVerticalStrut(3));
|
||||||
|
bankDisplay.add(bankDescription);
|
||||||
|
bankDisplay.add(Box.createVerticalStrut(7));
|
||||||
|
bankDisplay.add(createEquipmentActionButton("Reset bank display", plugin::restoreBankItems, false, false));
|
||||||
|
Dimension bankDisplaySize = bankDisplay.getPreferredSize();
|
||||||
|
bankDisplay.setMaximumSize(new Dimension(Integer.MAX_VALUE, bankDisplaySize.height));
|
||||||
|
controls.add(bankDisplay);
|
||||||
|
|
||||||
|
profiles.addActionListener(e ->
|
||||||
|
{
|
||||||
|
boolean selected = profiles.getSelectedItem() != null;
|
||||||
|
showLayout.setEnabled(selected);
|
||||||
|
renameLayout.setEnabled(selected);
|
||||||
|
deleteLayout.setEnabled(selected);
|
||||||
|
});
|
||||||
|
refreshProfiles.run();
|
||||||
|
panel.add(controls, BorderLayout.CENTER);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JButton createEquipmentActionButton(String text, Runnable action, boolean primary, boolean danger)
|
||||||
|
{
|
||||||
|
JButton button = new JButton(text);
|
||||||
|
button.setFont(primary ? FontManager.getRunescapeBoldFont() : FontManager.getRunescapeSmallFont());
|
||||||
|
button.setForeground(primary ? Ui.BG : danger ? Ui.DOWN : Ui.TEXT);
|
||||||
|
button.setBackground(primary ? Ui.ACCENT : Ui.CARD_HOVER);
|
||||||
|
button.setOpaque(true);
|
||||||
|
button.setContentAreaFilled(true);
|
||||||
|
button.setFocusPainted(true);
|
||||||
|
button.setBorder(BorderFactory.createCompoundBorder(
|
||||||
|
BorderFactory.createLineBorder(primary ? Ui.ACCENT : Ui.FAINT),
|
||||||
|
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
|
||||||
|
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
button.addActionListener(e -> action.run());
|
||||||
|
button.setPreferredSize(new Dimension(button.getPreferredSize().width, 30));
|
||||||
|
button.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));
|
||||||
|
button.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
public void showWiki()
|
public void showWiki()
|
||||||
{
|
{
|
||||||
cardLayout.show(container, VIEW_WIKI);
|
cardLayout.show(container, VIEW_WIKI);
|
||||||
@@ -293,8 +582,9 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
private static final String SKILLS = "skills";
|
private static final String SKILLS = "skills";
|
||||||
private static final String PROGRESS = "progress";
|
private static final String PROGRESS = "progress";
|
||||||
private static final String WIKI = "wiki";
|
private static final String WIKI = "wiki";
|
||||||
|
private static final String EQUIPMENT = "equipment";
|
||||||
private static final String HELPERS = "helpers";
|
private static final String HELPERS = "helpers";
|
||||||
private static final List<String> DEFAULT_ORDER = Arrays.asList(CURRENCY, WEALTH, LOOT, SKILLS, PROGRESS, WIKI, HELPERS);
|
private static final List<String> DEFAULT_ORDER = Arrays.asList(CURRENCY, WEALTH, LOOT, SKILLS, PROGRESS, WIKI, EQUIPMENT, HELPERS);
|
||||||
private static final Map<String, String> NAMES = new HashMap<>();
|
private static final Map<String, String> NAMES = new HashMap<>();
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -305,6 +595,7 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
NAMES.put(SKILLS, "Skills");
|
NAMES.put(SKILLS, "Skills");
|
||||||
NAMES.put(PROGRESS, "Achievements");
|
NAMES.put(PROGRESS, "Achievements");
|
||||||
NAMES.put(WIKI, "Wiki");
|
NAMES.put(WIKI, "Wiki");
|
||||||
|
NAMES.put(EQUIPMENT, "Equipment");
|
||||||
NAMES.put(HELPERS, "Helpers");
|
NAMES.put(HELPERS, "Helpers");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +605,7 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
private final LootSummaryCard lootSummaryCard;
|
private final LootSummaryCard lootSummaryCard;
|
||||||
private final SkillsCard skillsCard;
|
private final SkillsCard skillsCard;
|
||||||
private final WikiCard wikiCard;
|
private final WikiCard wikiCard;
|
||||||
|
private final EmptyCategoryCard equipmentCard;
|
||||||
private final ProgressCard progressCard;
|
private final ProgressCard progressCard;
|
||||||
private final HelpersCard helpersCard;
|
private final HelpersCard helpersCard;
|
||||||
private final JPanel topContainer;
|
private final JPanel topContainer;
|
||||||
@@ -370,6 +662,7 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
skillsCard = new SkillsCard(none);
|
skillsCard = new SkillsCard(none);
|
||||||
progressCard = new ProgressCard(none);
|
progressCard = new ProgressCard(none);
|
||||||
wikiCard = new WikiCard(none);
|
wikiCard = new WikiCard(none);
|
||||||
|
equipmentCard = new EmptyCategoryCard("Equipment");
|
||||||
helpersCard = new HelpersCard(plugin);
|
helpersCard = new HelpersCard(plugin);
|
||||||
refreshSkills();
|
refreshSkills();
|
||||||
refreshWiki();
|
refreshWiki();
|
||||||
@@ -402,6 +695,7 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
register(SKILLS, skillsCard, parent::showSkills);
|
register(SKILLS, skillsCard, parent::showSkills);
|
||||||
register(PROGRESS, progressCard, parent::showProgress);
|
register(PROGRESS, progressCard, parent::showProgress);
|
||||||
register(WIKI, wikiCard, parent::showWiki);
|
register(WIKI, wikiCard, parent::showWiki);
|
||||||
|
register(EQUIPMENT, equipmentCard, parent::showEquipment);
|
||||||
register(HELPERS, helpersCard, none());
|
register(HELPERS, helpersCard, none());
|
||||||
applyOptions();
|
applyOptions();
|
||||||
layoutCards();
|
layoutCards();
|
||||||
@@ -648,6 +942,9 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
case PROGRESS:
|
case PROGRESS:
|
||||||
parent.showProgress();
|
parent.showProgress();
|
||||||
break;
|
break;
|
||||||
|
case EQUIPMENT:
|
||||||
|
parent.showEquipment();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
parent.showWiki();
|
parent.showWiki();
|
||||||
break;
|
break;
|
||||||
@@ -701,7 +998,7 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
|
|
||||||
private void wealthItems(JPopupMenu menu)
|
private void wealthItems(JPopupMenu menu)
|
||||||
{
|
{
|
||||||
boolean isHourly = wealthPanel != null ? wealthPanel.isHourly() : (plugin.getTimeFilter().isCurrent() || plugin.getTimeFilter().getRange() == TimeRange.SESSION);
|
boolean isHourly = wealthPanel != null ? wealthPanel.isHourly() : (plugin.getTimeFilter().isCurrent() || (plugin.getTimeFilter().getRange() != null && plugin.getTimeFilter().getRange().isSession()));
|
||||||
if (isHourly)
|
if (isHourly)
|
||||||
{
|
{
|
||||||
JMenuItem reset = new JMenuItem("Reset Hourly");
|
JMenuItem reset = new JMenuItem("Reset Hourly");
|
||||||
@@ -802,5 +1099,60 @@ public class EssentialsPlusPanel extends PluginPanel
|
|||||||
SkillsPanel.resolve(plugin.getSkillStore(), null, plugin.getSkillKey()),
|
SkillsPanel.resolve(plugin.getSkillStore(), null, plugin.getSkillKey()),
|
||||||
plugin.getTimeFilter(), "Skills");
|
plugin.getTimeFilter(), "Skills");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class EmptyCategoryCard extends JComponent
|
||||||
|
{
|
||||||
|
private boolean hover;
|
||||||
|
|
||||||
|
EmptyCategoryCard(String title)
|
||||||
|
{
|
||||||
|
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
|
setToolTipText("Open " + title);
|
||||||
|
addMouseListener(new java.awt.event.MouseAdapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(java.awt.event.MouseEvent e)
|
||||||
|
{
|
||||||
|
hover = true;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(java.awt.event.MouseEvent e)
|
||||||
|
{
|
||||||
|
hover = false;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String title;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize()
|
||||||
|
{
|
||||||
|
return new Dimension(PluginPanel.PANEL_WIDTH - 16, 58);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g0)
|
||||||
|
{
|
||||||
|
Graphics2D g = Ui.prepare(g0);
|
||||||
|
g.setColor(hover ? Ui.CARD_HOVER : Ui.CARD);
|
||||||
|
g.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8);
|
||||||
|
Font bold = FontManager.getRunescapeBoldFont();
|
||||||
|
g.setFont(bold);
|
||||||
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
int baseline = (getHeight() - fm.getHeight()) / 2 + fm.getAscent();
|
||||||
|
g.setColor(Ui.ACCENT);
|
||||||
|
g.drawString(title, 10, baseline);
|
||||||
|
g.setFont(bold.deriveFont(14f));
|
||||||
|
g.setColor(hover ? Ui.ACCENT : Ui.FAINT);
|
||||||
|
Ui.drawRight(g, "›", getWidth() - 10, baseline);
|
||||||
|
Ui.paintDragOutline(g, this);
|
||||||
|
g.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,29 @@ import com.example.essentialsplus.EssentialsPlusPlugin;
|
|||||||
import com.example.essentialsplus.helpers.BlastFurnaceOverlay;
|
import com.example.essentialsplus.helpers.BlastFurnaceOverlay;
|
||||||
import com.example.essentialsplus.helpers.TimerTextSize;
|
import com.example.essentialsplus.helpers.TimerTextSize;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.Dialog;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.Graphics;
|
import java.awt.Graphics;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.event.KeyAdapter;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
import javax.swing.ButtonGroup;
|
||||||
import javax.swing.BorderFactory;
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.BoxLayout;
|
import javax.swing.BoxLayout;
|
||||||
|
import javax.swing.JButton;
|
||||||
import javax.swing.JCheckBox;
|
import javax.swing.JCheckBox;
|
||||||
import javax.swing.JCheckBoxMenuItem;
|
import javax.swing.JCheckBoxMenuItem;
|
||||||
|
import javax.swing.JDialog;
|
||||||
import javax.swing.JMenu;
|
import javax.swing.JMenu;
|
||||||
import javax.swing.JMenuItem;
|
import javax.swing.JMenuItem;
|
||||||
import javax.swing.JPopupMenu;
|
import javax.swing.JPopupMenu;
|
||||||
import javax.swing.JRadioButtonMenuItem;
|
import javax.swing.JRadioButtonMenuItem;
|
||||||
import javax.swing.SwingUtilities;
|
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import net.runelite.api.SoundEffectID;
|
||||||
|
import net.runelite.client.config.Keybind;
|
||||||
import net.runelite.client.ui.FontManager;
|
import net.runelite.client.ui.FontManager;
|
||||||
import net.runelite.client.ui.PluginPanel;
|
import net.runelite.client.ui.PluginPanel;
|
||||||
import net.runelite.client.ui.overlay.OverlayPosition;
|
import net.runelite.client.ui.overlay.OverlayPosition;
|
||||||
@@ -26,14 +35,16 @@ import net.runelite.client.ui.overlay.OverlayPosition;
|
|||||||
public class HelpersCard extends JPanel
|
public class HelpersCard extends JPanel
|
||||||
{
|
{
|
||||||
private final EssentialsPlusPlugin plugin;
|
private final EssentialsPlusPlugin plugin;
|
||||||
|
private final JCheckBox attackDistance = box("Attack Distance", "Outlines bow and crossbow range based on the active style. Right-click for options.");
|
||||||
private final JCheckBox attackTimer = box("Attack timer", "Ticks until your next attack, above your head. Right-click for options");
|
private final JCheckBox attackTimer = box("Attack timer", "Ticks until your next attack, above your head. Right-click for options");
|
||||||
private final JCheckBox enemyAttackStyle = box("Enemy attack style", "Melee, range or mage icon above NPCs attacking you. Right-click for options");
|
private final JCheckBox enemyAttackStyle = box("Enemy attack style", "Melee, range or mage icon above NPCs attacking you. Right-click for options");
|
||||||
private final JCheckBox itemPrices = box("Item prices", "GE and high alch price when hovering inventory items. Right-click for options");
|
|
||||||
private final JCheckBox enemyAttackTimer = box("Enemy attack timer", "Ticks until attacking NPCs hit next, above their heads. Right-click for options");
|
private final JCheckBox enemyAttackTimer = box("Enemy attack timer", "Ticks until attacking NPCs hit next, above their heads. Right-click for options");
|
||||||
private final JCheckBox treeCooldowns = box("Tree cooldowns", "Respawn-Countdown auf gefällten Bäumen + Rest-Zeit auf langlebigen Bäumen. Rechtsklick für Optionen");
|
private final JCheckBox itemInformation = box("Item information", "Shows item prices, banked quantities and charge information. Right-click for options.");
|
||||||
private final JCheckBox itemCharges = box("Item charges", "Charge-Zahl auf Item-Icons (Blowpipe, Trident, Tome, …) + Loot-Bag-Geld & Coal-Bag-Füllstand. Rechtsklick für Optionen");
|
private final JCheckBox treeCooldowns = box("Tree cooldowns", "Respawn countdown on felled trees and remaining chopping time. Right-click for options");
|
||||||
private final JCheckBox blastFurnace = box("Blast Furnace", "Highlights conveyor belt, bar dispenser & bank chest, shows coal in furnace, coffer time, and bars craftable from bank ores. Right-click for options");
|
private final JCheckBox blastFurnace = box("Blast Furnace", "Highlights conveyor belt, bar dispenser & bank chest, shows coal in furnace, coffer time, and bars craftable from bank ores. Right-click for options");
|
||||||
private final JCheckBox chatHelper = box("Chat helper", "Auto-Clear nach 1 Min, Duplikate (x2, x3), Spam-Filter, Namen-Highlight & Kopieren. Rechtsklick für Optionen");
|
private final JCheckBox chatHelper = box("Chat helper", "Auto-clear after one minute, duplicate collapsing, spam filtering, name highlighting, and copying. Right-click for options");
|
||||||
|
private final JCheckBox worldHopTracker = box("World hop tracker", "Show world-hop attempts per hour and today in chat. Right-click for hop hotkeys and filters.");
|
||||||
|
private final JCheckBox idleNotifier = box("Idle Notifier", "Blink and play a sound after a sustained activity ends. Right-click for timing, sound, and repeat options.");
|
||||||
|
|
||||||
public HelpersCard(EssentialsPlusPlugin plugin)
|
public HelpersCard(EssentialsPlusPlugin plugin)
|
||||||
{
|
{
|
||||||
@@ -48,6 +59,19 @@ public class HelpersCard extends JPanel
|
|||||||
title.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0));
|
title.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0));
|
||||||
add(title);
|
add(title);
|
||||||
|
|
||||||
|
attackDistance.addActionListener(e -> plugin.setHelperOption("attackDistance", attackDistance.isSelected()));
|
||||||
|
attackDistance.addMouseListener(new java.awt.event.MouseAdapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void mousePressed(java.awt.event.MouseEvent e)
|
||||||
|
{
|
||||||
|
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
||||||
|
{
|
||||||
|
attackDistanceMenu().show(attackDistance, e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(attackDistance);
|
||||||
attackTimer.addActionListener(e -> plugin.setHelperOption("attackTimer", attackTimer.isSelected()));
|
attackTimer.addActionListener(e -> plugin.setHelperOption("attackTimer", attackTimer.isSelected()));
|
||||||
enemyAttackTimer.addActionListener(e -> plugin.setHelperOption("enemyAttackTimer", enemyAttackTimer.isSelected()));
|
enemyAttackTimer.addActionListener(e -> plugin.setHelperOption("enemyAttackTimer", enemyAttackTimer.isSelected()));
|
||||||
optionsMenu(attackTimer, "attackTimer", "Attack timer", Color.WHITE);
|
optionsMenu(attackTimer, "attackTimer", "Attack timer", Color.WHITE);
|
||||||
@@ -67,19 +91,19 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
add(enemyAttackStyle);
|
add(enemyAttackStyle);
|
||||||
itemPrices.addActionListener(e -> plugin.setHelperOption("itemPrices", itemPrices.isSelected()));
|
itemInformation.addActionListener(e -> plugin.setHelperOption("itemInformation", itemInformation.isSelected()));
|
||||||
itemPrices.addMouseListener(new java.awt.event.MouseAdapter()
|
itemInformation.addMouseListener(new java.awt.event.MouseAdapter()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void mousePressed(java.awt.event.MouseEvent e)
|
public void mousePressed(java.awt.event.MouseEvent e)
|
||||||
{
|
{
|
||||||
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
||||||
{
|
{
|
||||||
priceMenu().show(itemPrices, e.getX(), e.getY());
|
itemInformationMenu().show(itemInformation, e.getX(), e.getY());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
add(itemPrices);
|
add(itemInformation);
|
||||||
treeCooldowns.addActionListener(e -> plugin.setHelperOption("treeCooldowns", treeCooldowns.isSelected()));
|
treeCooldowns.addActionListener(e -> plugin.setHelperOption("treeCooldowns", treeCooldowns.isSelected()));
|
||||||
treeCooldowns.addMouseListener(new java.awt.event.MouseAdapter()
|
treeCooldowns.addMouseListener(new java.awt.event.MouseAdapter()
|
||||||
{
|
{
|
||||||
@@ -93,19 +117,6 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
add(treeCooldowns);
|
add(treeCooldowns);
|
||||||
itemCharges.addActionListener(e -> plugin.setHelperOption("itemCharges", itemCharges.isSelected()));
|
|
||||||
itemCharges.addMouseListener(new java.awt.event.MouseAdapter()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void mousePressed(java.awt.event.MouseEvent e)
|
|
||||||
{
|
|
||||||
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
|
||||||
{
|
|
||||||
chargesMenu().show(itemCharges, e.getX(), e.getY());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
add(itemCharges);
|
|
||||||
blastFurnace.addActionListener(e -> plugin.setHelperOption("blastFurnace", blastFurnace.isSelected()));
|
blastFurnace.addActionListener(e -> plugin.setHelperOption("blastFurnace", blastFurnace.isSelected()));
|
||||||
blastFurnace.addMouseListener(new java.awt.event.MouseAdapter()
|
blastFurnace.addMouseListener(new java.awt.event.MouseAdapter()
|
||||||
{
|
{
|
||||||
@@ -132,20 +143,48 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
add(chatHelper);
|
add(chatHelper);
|
||||||
|
worldHopTracker.addActionListener(e -> plugin.setHelperOption("worldHopTracker", worldHopTracker.isSelected()));
|
||||||
|
worldHopTracker.addMouseListener(new java.awt.event.MouseAdapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void mousePressed(java.awt.event.MouseEvent e)
|
||||||
|
{
|
||||||
|
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
||||||
|
{
|
||||||
|
worldHopMenu().show(worldHopTracker, e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(worldHopTracker);
|
||||||
|
idleNotifier.addActionListener(e -> plugin.setHelperOption("idleNotifier", idleNotifier.isSelected()));
|
||||||
|
idleNotifier.addMouseListener(new java.awt.event.MouseAdapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void mousePressed(java.awt.event.MouseEvent e)
|
||||||
|
{
|
||||||
|
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
||||||
|
{
|
||||||
|
idleNotifierMenu().show(idleNotifier, e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(idleNotifier);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Re-reads the switches from the config (also changed in the RuneLite settings). */
|
/** Re-reads the switches from the config (also changed in the RuneLite settings). */
|
||||||
public void refresh()
|
public void refresh()
|
||||||
{
|
{
|
||||||
|
attackDistance.setSelected(plugin.isHelperOption("attackDistance"));
|
||||||
attackTimer.setSelected(plugin.isHelperOption("attackTimer"));
|
attackTimer.setSelected(plugin.isHelperOption("attackTimer"));
|
||||||
enemyAttackTimer.setSelected(plugin.isHelperOption("enemyAttackTimer"));
|
enemyAttackTimer.setSelected(plugin.isHelperOption("enemyAttackTimer"));
|
||||||
enemyAttackStyle.setSelected(plugin.isHelperOption("enemyAttackStyle"));
|
enemyAttackStyle.setSelected(plugin.isHelperOption("enemyAttackStyle"));
|
||||||
itemPrices.setSelected(plugin.isHelperOption("itemPrices"));
|
itemInformation.setSelected(plugin.isHelperOption("itemInformation"));
|
||||||
treeCooldowns.setSelected(plugin.isHelperOption("treeCooldowns"));
|
treeCooldowns.setSelected(plugin.isHelperOption("treeCooldowns"));
|
||||||
itemCharges.setSelected(plugin.isHelperOption("itemCharges"));
|
|
||||||
blastFurnace.setSelected(plugin.isHelperOption("blastFurnace"));
|
blastFurnace.setSelected(plugin.isHelperOption("blastFurnace"));
|
||||||
chatHelper.setSelected(plugin.isHelperOption("chatHelper"));
|
chatHelper.setSelected(plugin.isHelperOption("chatHelper"));
|
||||||
|
worldHopTracker.setSelected(plugin.isHelperOption("worldHopTracker"));
|
||||||
|
idleNotifier.setSelected(plugin.isHelperOption("idleNotifier"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +194,109 @@ public class HelpersCard extends JPanel
|
|||||||
private static final int[] STYLE_HEIGHTS = {10, 30, 60, 100, 150};
|
private static final int[] STYLE_HEIGHTS = {10, 30, 60, 100, 150};
|
||||||
private static final String[] STYLE_HEIGHT_LABELS = {"Low", "Normal", "High", "Very high", "Highest"};
|
private static final String[] STYLE_HEIGHT_LABELS = {"Low", "Normal", "High", "Very high", "Highest"};
|
||||||
|
|
||||||
|
private JPopupMenu attackDistanceMenu()
|
||||||
|
{
|
||||||
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
boolean hideOutOfCombat = plugin.getHelperValue("attackDistanceHideOutOfCombat", Boolean.class, false);
|
||||||
|
JCheckBoxMenuItem hideItem = new JCheckBoxMenuItem("Hide out of combat", hideOutOfCombat);
|
||||||
|
hideItem.addActionListener(e -> plugin.setHelperOption("attackDistanceHideOutOfCombat", hideItem.isSelected()));
|
||||||
|
menu.add(hideItem);
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPopupMenu worldHopMenu()
|
||||||
|
{
|
||||||
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
Keybind nextKey = plugin.getHelperValue("worldHopNextKey", Keybind.class, Keybind.NOT_SET);
|
||||||
|
JMenuItem next = new JMenuItem("Next world: " + nextKey);
|
||||||
|
next.addActionListener(e -> captureWorldHopKey("worldHopNextKey", "Next world"));
|
||||||
|
menu.add(next);
|
||||||
|
|
||||||
|
Keybind randomKey = plugin.getHelperValue("worldHopRandomKey", Keybind.class, Keybind.NOT_SET);
|
||||||
|
JMenuItem random = new JMenuItem("Random world: " + randomKey);
|
||||||
|
random.addActionListener(e -> captureWorldHopKey("worldHopRandomKey", "Random world"));
|
||||||
|
menu.add(random);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
boolean avoidDangerous = plugin.getHelperValue("worldHopAvoidDangerous", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem avoid = new JCheckBoxMenuItem("Avoid PvP and high-risk worlds", avoidDangerous);
|
||||||
|
avoid.addActionListener(e -> plugin.setHelperOption("worldHopAvoidDangerous", avoid.isSelected()));
|
||||||
|
menu.add(avoid);
|
||||||
|
JMenuItem bounty = new JMenuItem("Bounty worlds are always skipped");
|
||||||
|
bounty.setEnabled(false);
|
||||||
|
menu.add(bounty);
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void captureWorldHopKey(String key, String action)
|
||||||
|
{
|
||||||
|
JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor(worldHopTracker), "Set " + action + " hotkey", Dialog.ModalityType.APPLICATION_MODAL);
|
||||||
|
JPanel panel = new JPanel(new java.awt.BorderLayout(8, 8));
|
||||||
|
panel.setBorder(BorderFactory.createEmptyBorder(12, 12, 8, 12));
|
||||||
|
panel.setFocusable(true);
|
||||||
|
JLabel prompt = new JLabel("Press a key or key combination. Esc cancels.");
|
||||||
|
panel.add(prompt, java.awt.BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JPanel buttons = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT, 6, 0));
|
||||||
|
JButton clear = new JButton("Clear");
|
||||||
|
clear.setFocusable(false);
|
||||||
|
clear.addActionListener(e ->
|
||||||
|
{
|
||||||
|
plugin.setHelperOption(key, Keybind.NOT_SET);
|
||||||
|
dialog.dispose();
|
||||||
|
});
|
||||||
|
JButton cancel = new JButton("Cancel");
|
||||||
|
cancel.setFocusable(false);
|
||||||
|
cancel.addActionListener(e -> dialog.dispose());
|
||||||
|
buttons.add(clear);
|
||||||
|
buttons.add(cancel);
|
||||||
|
panel.add(buttons, java.awt.BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
panel.addKeyListener(new KeyAdapter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void keyPressed(KeyEvent e)
|
||||||
|
{
|
||||||
|
if (e.getKeyCode() == KeyEvent.VK_ESCAPE)
|
||||||
|
{
|
||||||
|
dialog.dispose();
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Keybind.getModifierForKeyCode(e.getExtendedKeyCode()) != null)
|
||||||
|
{
|
||||||
|
prompt.setText("Now press the key while holding the modifier.");
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.getExtendedKeyCode() == KeyEvent.VK_UNDEFINED)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Keybind selected = new Keybind(e);
|
||||||
|
String otherKeyName = "worldHopNextKey".equals(key) ? "worldHopRandomKey" : "worldHopNextKey";
|
||||||
|
Keybind otherKey = plugin.getHelperValue(otherKeyName, Keybind.class, Keybind.NOT_SET);
|
||||||
|
if (!Keybind.NOT_SET.equals(otherKey) && otherKey.equals(selected))
|
||||||
|
{
|
||||||
|
JOptionPane.showMessageDialog(dialog, "That hotkey is already assigned to the other hop action.", "Hotkey already used", JOptionPane.WARNING_MESSAGE);
|
||||||
|
e.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.setHelperOption(key, selected);
|
||||||
|
dialog.dispose();
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
panel.setFocusTraversalKeysEnabled(false);
|
||||||
|
dialog.setContentPane(panel);
|
||||||
|
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
|
||||||
|
dialog.pack();
|
||||||
|
dialog.setLocationRelativeTo(worldHopTracker);
|
||||||
|
SwingUtilities.invokeLater(panel::requestFocusInWindow);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
/** Right-click on a switch: look of that timer (bar, size, colors, height). */
|
/** Right-click on a switch: look of that timer (bar, size, colors, height). */
|
||||||
private void optionsMenu(JCheckBox box, String prefix, String name, Color defColor)
|
private void optionsMenu(JCheckBox box, String prefix, String name, Color defColor)
|
||||||
{
|
{
|
||||||
@@ -218,18 +360,6 @@ public class HelpersCard extends JPanel
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JPopupMenu priceMenu()
|
|
||||||
{
|
|
||||||
JPopupMenu menu = new JPopupMenu();
|
|
||||||
for (String[] opt : new String[][]{{"itemPricesGe", "Show GE price"}, {"itemPricesHa", "Show HA price"}})
|
|
||||||
{
|
|
||||||
boolean on = plugin.getHelperValue(opt[0], Boolean.class, true);
|
|
||||||
JCheckBoxMenuItem item = new JCheckBoxMenuItem(opt[1], on);
|
|
||||||
item.addActionListener(e -> plugin.setHelperOption(opt[0], on ? false : null));
|
|
||||||
menu.add(item);
|
|
||||||
}
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JPopupMenu styleMenu()
|
private JPopupMenu styleMenu()
|
||||||
{
|
{
|
||||||
@@ -267,15 +397,15 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final int[] TREE_HEIGHTS = {20, 50, 80, 120};
|
private static final int[] TREE_HEIGHTS = {20, 50, 80, 120};
|
||||||
private static final String[] TREE_HEIGHT_LABELS = {"Niedrig", "Normal", "Hoch", "Sehr hoch"};
|
private static final String[] TREE_HEIGHT_LABELS = {"Low", "Normal", "High", "Very high"};
|
||||||
|
|
||||||
private JPopupMenu treeMenu()
|
private JPopupMenu treeMenu()
|
||||||
{
|
{
|
||||||
JPopupMenu menu = new JPopupMenu();
|
JPopupMenu menu = new JPopupMenu();
|
||||||
for (String[] opt : new String[][]{
|
for (String[] opt : new String[][]{
|
||||||
{"treeCooldownRespawn", "Respawn-Countdown anzeigen"},
|
{"treeCooldownRespawn", "Show respawn countdown"},
|
||||||
{"treeCooldownChop", "Hack-Restzeit anzeigen"},
|
{"treeCooldownChop", "Show remaining chopping time"},
|
||||||
{"treeCooldownBar", "Fortschrittsbalken anzeigen"}
|
{"treeCooldownBar", "Show progress bar"}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
boolean on = plugin.getHelperValue(opt[0], Boolean.class, !opt[0].equals("treeCooldownBar"));
|
boolean on = plugin.getHelperValue(opt[0], Boolean.class, !opt[0].equals("treeCooldownBar"));
|
||||||
@@ -285,7 +415,7 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
JMenu size = new JMenu("Textgröße");
|
JMenu size = new JMenu("Text size");
|
||||||
TimerTextSize currentSize = plugin.getHelperValue("treeCooldownSize", TimerTextSize.class, TimerTextSize.NORMAL);
|
TimerTextSize currentSize = plugin.getHelperValue("treeCooldownSize", TimerTextSize.class, TimerTextSize.NORMAL);
|
||||||
for (TimerTextSize s : TimerTextSize.values())
|
for (TimerTextSize s : TimerTextSize.values())
|
||||||
{
|
{
|
||||||
@@ -293,7 +423,7 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
menu.add(size);
|
menu.add(size);
|
||||||
|
|
||||||
JMenu height = new JMenu("Höhe");
|
JMenu height = new JMenu("Height");
|
||||||
int currentHeight = plugin.getHelperValue("treeCooldownOffset", Integer.class, 50);
|
int currentHeight = plugin.getHelperValue("treeCooldownOffset", Integer.class, 50);
|
||||||
for (int i = 0; i < TREE_HEIGHTS.length; i++)
|
for (int i = 0; i < TREE_HEIGHTS.length; i++)
|
||||||
{
|
{
|
||||||
@@ -303,11 +433,11 @@ public class HelpersCard extends JPanel
|
|||||||
menu.add(height);
|
menu.add(height);
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
menu.add(colorItem("Farbe...", "treeCooldownColor", "Tree Cooldown Farbe", new Color(0x8FBF3F)));
|
menu.add(colorItem("Color...", "treeCooldownColor", "Tree Cooldown color", new Color(0x8FBF3F)));
|
||||||
menu.add(colorItem("Farbe bei Bereit...", "treeCooldownReadyColor", "Tree Cooldown Farbe bei Bereit", READY));
|
menu.add(colorItem("Ready color...", "treeCooldownReadyColor", "Tree Cooldown ready color", READY));
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
JMenuItem reset = new JMenuItem("Aussehen zurücksetzen");
|
JMenuItem reset = new JMenuItem("Reset look");
|
||||||
reset.addActionListener(e ->
|
reset.addActionListener(e ->
|
||||||
{
|
{
|
||||||
for (String key : new String[]{"treeCooldownRespawn", "treeCooldownChop", "treeCooldownBar", "treeCooldownSize", "treeCooldownOffset", "treeCooldownColor", "treeCooldownReadyColor"})
|
for (String key : new String[]{"treeCooldownRespawn", "treeCooldownChop", "treeCooldownBar", "treeCooldownSize", "treeCooldownOffset", "treeCooldownColor", "treeCooldownReadyColor"})
|
||||||
@@ -319,51 +449,96 @@ public class HelpersCard extends JPanel
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int[] CHARGES_LOW_VALUES = {0, 10, 50, 100, 500, 1000};
|
private static final int[] INFO_LOW_VALUES = {0, 10, 50, 100, 500, 1000};
|
||||||
private static final String[] CHARGES_LOW_LABELS = {"Aus (0)", "≤ 10", "≤ 50", "≤ 100", "≤ 500", "≤ 1000"};
|
private static final String[] INFO_LOW_LABELS = {"Off", "≤ 10", "≤ 50", "≤ 100", "≤ 500", "≤ 1000"};
|
||||||
|
|
||||||
private JPopupMenu chargesMenu()
|
private JPopupMenu itemInformationMenu()
|
||||||
{
|
{
|
||||||
JPopupMenu menu = new JPopupMenu();
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
|
||||||
for (String[] opt : new String[][]{
|
// Information types
|
||||||
{"itemChargesWeapons", "Waffen & Rüstung anzeigen"},
|
boolean geOn = plugin.getHelperValue("itemInformationGe", Boolean.class, true);
|
||||||
{"itemChargesBags", "Looting- & Coal-Bag anzeigen"},
|
JCheckBoxMenuItem geItem = new JCheckBoxMenuItem("Show GE price", geOn);
|
||||||
{"itemChargesBank", "In der Bank anzeigen"}
|
geItem.addActionListener(e -> plugin.setHelperOption("itemInformationGe", !geOn));
|
||||||
})
|
menu.add(geItem);
|
||||||
{
|
|
||||||
boolean on = plugin.getHelperValue(opt[0], Boolean.class, true);
|
boolean haOn = plugin.getHelperValue("itemInformationHa", Boolean.class, true);
|
||||||
JCheckBoxMenuItem item = new JCheckBoxMenuItem(opt[1], on);
|
JCheckBoxMenuItem haItem = new JCheckBoxMenuItem("Show High Alchemy value", haOn);
|
||||||
item.addActionListener(e -> plugin.setHelperOption(opt[0], on ? false : true));
|
haItem.addActionListener(e -> plugin.setHelperOption("itemInformationHa", !haOn));
|
||||||
menu.add(item);
|
menu.add(haItem);
|
||||||
}
|
|
||||||
|
boolean bankedOn = plugin.getHelperValue("itemInformationBanked", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem bankedItem = new JCheckBoxMenuItem("Show banked quantity", bankedOn);
|
||||||
|
bankedItem.addActionListener(e -> plugin.setHelperOption("itemInformationBanked", !bankedOn));
|
||||||
|
menu.add(bankedItem);
|
||||||
|
|
||||||
|
boolean chargesOn = plugin.getHelperValue("itemInformationCharges", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem chargesItem = new JCheckBoxMenuItem("Show item charges", chargesOn);
|
||||||
|
chargesItem.addActionListener(e -> plugin.setHelperOption("itemInformationCharges", !chargesOn));
|
||||||
|
menu.add(chargesItem);
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
JMenu size = new JMenu("Textgröße");
|
// Charge categories (relevant when charges are enabled)
|
||||||
TimerTextSize currentSize = plugin.getHelperValue("itemChargesSize", TimerTextSize.class, TimerTextSize.SMALL);
|
boolean weaponsOn = plugin.getHelperValue("itemInformationChargesWeapons", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem weaponsItem = new JCheckBoxMenuItem("Weapons and armor", weaponsOn);
|
||||||
|
weaponsItem.setEnabled(chargesOn);
|
||||||
|
weaponsItem.addActionListener(e -> plugin.setHelperOption("itemInformationChargesWeapons", !weaponsOn));
|
||||||
|
menu.add(weaponsItem);
|
||||||
|
|
||||||
|
boolean bagsOn = plugin.getHelperValue("itemInformationChargesBags", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem bagsItem = new JCheckBoxMenuItem("Bags and containers", bagsOn);
|
||||||
|
bagsItem.setEnabled(chargesOn);
|
||||||
|
bagsItem.addActionListener(e -> plugin.setHelperOption("itemInformationChargesBags", !bagsOn));
|
||||||
|
menu.add(bagsItem);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// Display locations
|
||||||
|
boolean bankHover = plugin.getHelperValue("itemInformationBank", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem bankHoverItem = new JCheckBoxMenuItem("Show in bank", bankHover);
|
||||||
|
bankHoverItem.addActionListener(e -> plugin.setHelperOption("itemInformationBank", !bankHover));
|
||||||
|
menu.add(bankHoverItem);
|
||||||
|
|
||||||
|
boolean equipOn = plugin.getHelperValue("itemInformationEquipment", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem equipItem = new JCheckBoxMenuItem("Show charges in equipment", equipOn);
|
||||||
|
equipItem.setEnabled(chargesOn);
|
||||||
|
equipItem.addActionListener(e -> plugin.setHelperOption("itemInformationEquipment", !equipOn));
|
||||||
|
menu.add(equipItem);
|
||||||
|
|
||||||
|
boolean bankCharges = plugin.getHelperValue("itemInformationChargesBank", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem bankChargesItem = new JCheckBoxMenuItem("Show charges in bank", bankCharges);
|
||||||
|
bankChargesItem.setEnabled(chargesOn);
|
||||||
|
bankChargesItem.addActionListener(e -> plugin.setHelperOption("itemInformationChargesBank", !bankCharges));
|
||||||
|
menu.add(bankChargesItem);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// Appearance & Warnings
|
||||||
|
JMenu size = new JMenu("Text size");
|
||||||
|
TimerTextSize currentSize = plugin.getHelperValue("itemInformationSize", TimerTextSize.class, TimerTextSize.SMALL);
|
||||||
for (TimerTextSize s : TimerTextSize.values())
|
for (TimerTextSize s : TimerTextSize.values())
|
||||||
{
|
{
|
||||||
size.add(radio(s.toString(), s == currentSize, () -> plugin.setHelperOption("itemChargesSize", s)));
|
size.add(radio(s.toString(), s == currentSize, () -> plugin.setHelperOption("itemInformationSize", s)));
|
||||||
}
|
}
|
||||||
menu.add(size);
|
menu.add(size);
|
||||||
|
|
||||||
JMenu lowThreshold = new JMenu("Warnung bei wenig Ladung");
|
JMenu lowThreshold = new JMenu("Low-charge warning");
|
||||||
int currentLowAt = plugin.getHelperValue("itemChargesLowAt", Integer.class, 0);
|
int currentLowAt = plugin.getHelperValue("itemInformationLowAt", Integer.class, 0);
|
||||||
for (int i = 0; i < CHARGES_LOW_VALUES.length; i++)
|
for (int i = 0; i < INFO_LOW_VALUES.length; i++)
|
||||||
{
|
{
|
||||||
int value = CHARGES_LOW_VALUES[i];
|
int value = INFO_LOW_VALUES[i];
|
||||||
lowThreshold.add(radio(CHARGES_LOW_LABELS[i], value == currentLowAt, () -> plugin.setHelperOption("itemChargesLowAt", value)));
|
lowThreshold.add(radio(INFO_LOW_LABELS[i], value == currentLowAt, () -> plugin.setHelperOption("itemInformationLowAt", value)));
|
||||||
}
|
}
|
||||||
menu.add(lowThreshold);
|
menu.add(lowThreshold);
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
menu.add(colorItem("Farbe...", "itemChargesColor", "Item Charges Primärfarbe", Color.WHITE));
|
menu.add(colorItem("Primary color...", "itemInformationColor", "Item info primary color", Color.WHITE));
|
||||||
menu.add(colorItem("Sekundärfarbe...", "itemChargesSecondaryColor", "Item Charges Sekundärfarbe", new Color(0xC0C0C0)));
|
menu.add(colorItem("Secondary color...", "itemInformationSecondaryColor", "Item info secondary color", new Color(0xC0C0C0)));
|
||||||
menu.add(colorItem("Warnfarbe...", "itemChargesLowColor", "Item Charges Warnfarbe", new Color(0xFF3333)));
|
menu.add(colorItem("Warning color...", "itemInformationLowColor", "Item info warning color", new Color(0xFF3333)));
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
JMenuItem forgetCharges = new JMenuItem("Gespeicherte Ladungen zurücksetzen");
|
JMenuItem forgetCharges = new JMenuItem("Reset saved charges");
|
||||||
forgetCharges.addActionListener(e ->
|
forgetCharges.addActionListener(e ->
|
||||||
{
|
{
|
||||||
if (plugin.getItemCharges() != null)
|
if (plugin.getItemCharges() != null)
|
||||||
@@ -373,7 +548,7 @@ public class HelpersCard extends JPanel
|
|||||||
});
|
});
|
||||||
menu.add(forgetCharges);
|
menu.add(forgetCharges);
|
||||||
|
|
||||||
JMenuItem forgetCoal = new JMenuItem("Coal bag vergessen");
|
JMenuItem forgetCoal = new JMenuItem("Forget coal bag");
|
||||||
forgetCoal.addActionListener(e ->
|
forgetCoal.addActionListener(e ->
|
||||||
{
|
{
|
||||||
if (plugin.getItemCharges() != null)
|
if (plugin.getItemCharges() != null)
|
||||||
@@ -383,10 +558,16 @@ public class HelpersCard extends JPanel
|
|||||||
});
|
});
|
||||||
menu.add(forgetCoal);
|
menu.add(forgetCoal);
|
||||||
|
|
||||||
JMenuItem reset = new JMenuItem("Aussehen zurücksetzen");
|
menu.addSeparator();
|
||||||
|
JMenuItem reset = new JMenuItem("Reset appearance");
|
||||||
reset.addActionListener(e ->
|
reset.addActionListener(e ->
|
||||||
{
|
{
|
||||||
for (String key : new String[]{"itemChargesWeapons", "itemChargesBags", "itemChargesBank", "itemChargesSize", "itemChargesLowAt", "itemChargesColor", "itemChargesSecondaryColor", "itemChargesLowColor"})
|
for (String key : new String[]{
|
||||||
|
"itemInformationSize", "itemInformationLowAt", "itemInformationColor",
|
||||||
|
"itemInformationSecondaryColor", "itemInformationLowColor",
|
||||||
|
"itemChargesSize", "itemChargesLowAt", "itemChargesColor",
|
||||||
|
"itemChargesSecondaryColor", "itemChargesLowColor"
|
||||||
|
})
|
||||||
{
|
{
|
||||||
plugin.setHelperOption(key, null);
|
plugin.setHelperOption(key, null);
|
||||||
}
|
}
|
||||||
@@ -491,18 +672,18 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final int[] CHAT_CLEAR_TIMES = {0, 30, 60, 120, 300};
|
private static final int[] CHAT_CLEAR_TIMES = {0, 30, 60, 120, 300};
|
||||||
private static final String[] CHAT_CLEAR_TIME_LABELS = {"Aus (0s)", "30 Sekunden", "1 Minute (Standard)", "2 Minuten", "5 Minuten"};
|
private static final String[] CHAT_CLEAR_TIME_LABELS = {"Off (0s)", "30 seconds", "1 minute (default)", "2 minutes", "5 minutes"};
|
||||||
|
|
||||||
private JPopupMenu chatMenu()
|
private JPopupMenu chatMenu()
|
||||||
{
|
{
|
||||||
JPopupMenu menu = new JPopupMenu();
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
|
||||||
boolean autoClear = plugin.getHelperValue("chatAutoClear", Boolean.class, true);
|
boolean autoClear = plugin.getHelperValue("chatAutoClear", Boolean.class, true);
|
||||||
JCheckBoxMenuItem autoClearItem = new JCheckBoxMenuItem("Automatisch aufräumen", autoClear);
|
JCheckBoxMenuItem autoClearItem = new JCheckBoxMenuItem("Auto-clear messages", autoClear);
|
||||||
autoClearItem.addActionListener(e -> plugin.setHelperOption("chatAutoClear", !autoClear));
|
autoClearItem.addActionListener(e -> plugin.setHelperOption("chatAutoClear", !autoClear));
|
||||||
menu.add(autoClearItem);
|
menu.add(autoClearItem);
|
||||||
|
|
||||||
JMenu clearTimeMenu = new JMenu("Auto-Clear Zeit");
|
JMenu clearTimeMenu = new JMenu("Auto-clear delay");
|
||||||
int currentTime = plugin.getHelperValue("chatAutoClearTime", Integer.class, 60);
|
int currentTime = plugin.getHelperValue("chatAutoClearTime", Integer.class, 60);
|
||||||
for (int i = 0; i < CHAT_CLEAR_TIMES.length; i++)
|
for (int i = 0; i < CHAT_CLEAR_TIMES.length; i++)
|
||||||
{
|
{
|
||||||
@@ -511,12 +692,12 @@ public class HelpersCard extends JPanel
|
|||||||
}
|
}
|
||||||
menu.add(clearTimeMenu);
|
menu.add(clearTimeMenu);
|
||||||
|
|
||||||
JMenu typesMenu = new JMenu("Was wird aufgeräumt?");
|
JMenu typesMenu = new JMenu("Message types to clear");
|
||||||
for (String[] opt : new String[][]{
|
for (String[] opt : new String[][]{
|
||||||
{"chatClearGame", "Spiel- & System-Meldungen", "true"},
|
{"chatClearGame", "Game and system messages", "true"},
|
||||||
{"chatClearPublic", "Öffentlicher Chat (Public)", "true"},
|
{"chatClearPublic", "Public chat", "true"},
|
||||||
{"chatClearPrivate", "Private Nachrichten (PM)", "false"},
|
{"chatClearPrivate", "Private messages (PMs)", "false"},
|
||||||
{"chatClearClan", "Clan- & Freunde-Chat", "false"}
|
{"chatClearClan", "Clan and friends chat", "false"}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
boolean def = Boolean.parseBoolean(opt[2]);
|
boolean def = Boolean.parseBoolean(opt[2]);
|
||||||
@@ -530,10 +711,10 @@ public class HelpersCard extends JPanel
|
|||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
for (String[] opt : new String[][]{
|
for (String[] opt : new String[][]{
|
||||||
{"chatCollapseDuplicates", "Duplikate zusammenfassen (x2, x3...)"},
|
{"chatCollapseDuplicates", "Collapse duplicates (x2, x3...)"},
|
||||||
{"chatFilterSpam", "Spam & Systemlärm filtern"},
|
{"chatFilterSpam", "Filter spam and game noise"},
|
||||||
{"chatHighlightName", "Eigenen Namen hervorheben"},
|
{"chatHighlightName", "Highlight your name"},
|
||||||
{"chatCopyToClipboard", "Rechtsklick 'Kopieren' aktivieren"}
|
{"chatCopyToClipboard", "Enable right-click copying"}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
boolean on = plugin.getHelperValue(opt[0], Boolean.class, true);
|
boolean on = plugin.getHelperValue(opt[0], Boolean.class, true);
|
||||||
@@ -542,11 +723,12 @@ public class HelpersCard extends JPanel
|
|||||||
menu.add(item);
|
menu.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.add(colorItem("Highlight-Farbe...", "chatHighlightColor", "Chat Highlight-Farbe", new Color(0xFFD700)));
|
menu.add(colorItem("Highlight color...", "chatHighlightColor", "Chat highlight color", new Color(0xFFD700)));
|
||||||
|
menu.add(colorItem("Clan message color...", "chatClanMessageColor", "Clan message color", new Color(0x66FFFF)));
|
||||||
|
|
||||||
menu.addSeparator();
|
menu.addSeparator();
|
||||||
|
|
||||||
JMenuItem clearNow = new JMenuItem("Chat jetzt leeren");
|
JMenuItem clearNow = new JMenuItem("Clear chat now");
|
||||||
clearNow.addActionListener(e ->
|
clearNow.addActionListener(e ->
|
||||||
{
|
{
|
||||||
if (plugin.getChatHelper() != null)
|
if (plugin.getChatHelper() != null)
|
||||||
@@ -556,13 +738,13 @@ public class HelpersCard extends JPanel
|
|||||||
});
|
});
|
||||||
menu.add(clearNow);
|
menu.add(clearNow);
|
||||||
|
|
||||||
JMenuItem reset = new JMenuItem("Einstellungen zurücksetzen");
|
JMenuItem reset = new JMenuItem("Reset settings");
|
||||||
reset.addActionListener(e ->
|
reset.addActionListener(e ->
|
||||||
{
|
{
|
||||||
for (String key : new String[]{
|
for (String key : new String[]{
|
||||||
"chatAutoClear", "chatAutoClearTime", "chatClearGame", "chatClearPublic",
|
"chatAutoClear", "chatAutoClearTime", "chatClearGame", "chatClearPublic",
|
||||||
"chatClearPrivate", "chatClearClan", "chatCollapseDuplicates",
|
"chatClearPrivate", "chatClearClan", "chatCollapseDuplicates",
|
||||||
"chatFilterSpam", "chatHighlightName", "chatHighlightColor", "chatCopyToClipboard"
|
"chatFilterSpam", "chatHighlightName", "chatHighlightColor", "chatClanMessageColor", "chatCopyToClipboard"
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
plugin.setHelperOption(key, null);
|
plugin.setHelperOption(key, null);
|
||||||
@@ -573,6 +755,115 @@ public class HelpersCard extends JPanel
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JPopupMenu idleNotifierMenu()
|
||||||
|
{
|
||||||
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
menu.add(secondsMenu("Minimum action", "idleNotifierMinActionSeconds", 5, new int[]{5, 10, 15, 30, 60, 120}));
|
||||||
|
menu.add(secondsMenu("Idle delay", "idleNotifierIdleSeconds", 5, new int[]{1, 2, 3, 5, 10, 15, 30, 60}));
|
||||||
|
|
||||||
|
JMenu blinkDuration = new JMenu("Blink duration");
|
||||||
|
int currentDuration = plugin.getHelperValue("idleNotifierFlashDurationMs", Integer.class, 1500);
|
||||||
|
int[] durations = {750, 1000, 1500, 2000, 3000, 5000};
|
||||||
|
ButtonGroup durationGroup = new ButtonGroup();
|
||||||
|
for (int duration : durations)
|
||||||
|
{
|
||||||
|
String label = duration % 1000 == 0 ? (duration / 1000) + " seconds" : (duration / 1000.0) + " seconds";
|
||||||
|
JRadioButtonMenuItem item = radio(label, duration == currentDuration, () -> plugin.setHelperOption("idleNotifierFlashDurationMs", duration));
|
||||||
|
durationGroup.add(item);
|
||||||
|
blinkDuration.add(item);
|
||||||
|
}
|
||||||
|
menu.add(blinkDuration);
|
||||||
|
|
||||||
|
JMenu blinkInterval = new JMenu("Blink speed");
|
||||||
|
int currentInterval = plugin.getHelperValue("idleNotifierBlinkIntervalMs", Integer.class, 250);
|
||||||
|
int[] intervals = {100, 150, 250, 400, 600};
|
||||||
|
ButtonGroup intervalGroup = new ButtonGroup();
|
||||||
|
for (int interval : intervals)
|
||||||
|
{
|
||||||
|
JRadioButtonMenuItem item = radio(interval + " ms", interval == currentInterval, () -> plugin.setHelperOption("idleNotifierBlinkIntervalMs", interval));
|
||||||
|
intervalGroup.add(item);
|
||||||
|
blinkInterval.add(item);
|
||||||
|
}
|
||||||
|
menu.add(blinkInterval);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
boolean soundOn = plugin.getHelperValue("idleNotifierSound", Boolean.class, true);
|
||||||
|
JCheckBoxMenuItem sound = new JCheckBoxMenuItem("Play sound", soundOn);
|
||||||
|
sound.addActionListener(e -> plugin.setHelperOption("idleNotifierSound", sound.isSelected()));
|
||||||
|
menu.add(sound);
|
||||||
|
|
||||||
|
boolean repeatOn = plugin.getHelperValue("idleNotifierRepeat", Boolean.class, false);
|
||||||
|
JCheckBoxMenuItem repeat = new JCheckBoxMenuItem("Repeat while idle", repeatOn);
|
||||||
|
repeat.addActionListener(e -> plugin.setHelperOption("idleNotifierRepeat", repeat.isSelected()));
|
||||||
|
menu.add(repeat);
|
||||||
|
menu.add(secondsMenu("Repeat every", "idleNotifierRepeatIntervalSeconds", 5, new int[]{1, 2, 3, 5, 10, 15, 30, 60}));
|
||||||
|
menu.add(secondsMenu("Repeat for", "idleNotifierRepeatDurationSeconds", 15, new int[]{5, 10, 15, 30, 60, 120, 300}));
|
||||||
|
|
||||||
|
JMenu soundChoice = new JMenu("Sound effect");
|
||||||
|
int currentSoundId = plugin.getHelperValue("idleNotifierSoundId", Integer.class, SoundEffectID.GE_ADD_OFFER_DINGALING);
|
||||||
|
int[] soundIds = {SoundEffectID.GE_ADD_OFFER_DINGALING, SoundEffectID.UI_BOOP, SoundEffectID.GE_COIN_TINKLE, SoundEffectID.TOWN_CRIER_BELL_DING};
|
||||||
|
String[] soundLabels = {"GE ding", "Soft boop", "Coin tinkle", "Bell"};
|
||||||
|
ButtonGroup soundGroup = new ButtonGroup();
|
||||||
|
for (int i = 0; i < soundIds.length; i++)
|
||||||
|
{
|
||||||
|
int soundId = soundIds[i];
|
||||||
|
JRadioButtonMenuItem item = radio(soundLabels[i], soundId == currentSoundId, () -> plugin.setHelperOption("idleNotifierSoundId", soundId));
|
||||||
|
soundGroup.add(item);
|
||||||
|
soundChoice.add(item);
|
||||||
|
}
|
||||||
|
menu.add(soundChoice);
|
||||||
|
|
||||||
|
JMenu volume = new JMenu("Sound volume");
|
||||||
|
int currentVolume = plugin.getHelperValue("idleNotifierSoundVolume", Integer.class, 64);
|
||||||
|
int[] volumes = {32, 64, 96, 127};
|
||||||
|
String[] volumeLabels = {"Quiet", "Normal", "Loud", "Full"};
|
||||||
|
ButtonGroup volumeGroup = new ButtonGroup();
|
||||||
|
for (int i = 0; i < volumes.length; i++)
|
||||||
|
{
|
||||||
|
int value = volumes[i];
|
||||||
|
JRadioButtonMenuItem item = radio(volumeLabels[i], value == currentVolume, () -> plugin.setHelperOption("idleNotifierSoundVolume", value));
|
||||||
|
volumeGroup.add(item);
|
||||||
|
volume.add(item);
|
||||||
|
}
|
||||||
|
menu.add(volume);
|
||||||
|
|
||||||
|
JMenuItem testSound = new JMenuItem("Test sound");
|
||||||
|
testSound.addActionListener(e -> plugin.previewIdleNotifierSound());
|
||||||
|
menu.add(testSound);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
JMenuItem reset = new JMenuItem("Reset options");
|
||||||
|
reset.addActionListener(e ->
|
||||||
|
{
|
||||||
|
for (String key : new String[]{
|
||||||
|
"idleNotifierMinActionSeconds", "idleNotifierIdleSeconds", "idleNotifierFlashDurationMs",
|
||||||
|
"idleNotifierBlinkIntervalMs", "idleNotifierSound", "idleNotifierSoundId", "idleNotifierSoundVolume",
|
||||||
|
"idleNotifierRepeat", "idleNotifierRepeatIntervalSeconds", "idleNotifierRepeatDurationSeconds"
|
||||||
|
})
|
||||||
|
{
|
||||||
|
plugin.setHelperOption(key, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.add(reset);
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JMenu secondsMenu(String title, String key, int defaultValue, int[] values)
|
||||||
|
{
|
||||||
|
JMenu menu = new JMenu(title);
|
||||||
|
int current = plugin.getHelperValue(key, Integer.class, defaultValue);
|
||||||
|
ButtonGroup group = new ButtonGroup();
|
||||||
|
for (int value : values)
|
||||||
|
{
|
||||||
|
JRadioButtonMenuItem item = radio(value + (value == 1 ? " second" : " seconds"), value == current,
|
||||||
|
() -> plugin.setHelperOption(key, value));
|
||||||
|
group.add(item);
|
||||||
|
menu.add(item);
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Checkbox for a switch that is on by default (turning it on stores the default explicitly). */
|
/** Checkbox for a switch that is on by default (turning it on stores the default explicitly). */
|
||||||
private JCheckBoxMenuItem toggle(String key, String label)
|
private JCheckBoxMenuItem toggle(String key, String label)
|
||||||
|
|||||||
@@ -8,21 +8,26 @@ import java.awt.Graphics;
|
|||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.IntConsumer;
|
import java.util.function.IntConsumer;
|
||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
import net.runelite.client.ui.FontManager;
|
import net.runelite.client.ui.FontManager;
|
||||||
|
|
||||||
/** Segmented control (time range, chart style). */
|
/** Segmented control (time range, chart style). */
|
||||||
public class Segmented extends JComponent
|
public class Segmented extends JComponent
|
||||||
{
|
{
|
||||||
private final String[] labels;
|
private String[] labels;
|
||||||
private final IntConsumer onSelect;
|
private final IntConsumer onSelect;
|
||||||
|
private BiConsumer<Integer, MouseEvent> onRightClick;
|
||||||
private int selected;
|
private int selected;
|
||||||
private int hover = -1;
|
private int hover = -1;
|
||||||
|
|
||||||
public Segmented(String[] labels, int selected, IntConsumer onSelect)
|
public Segmented(String[] labels, int selected, IntConsumer onSelect)
|
||||||
{
|
{
|
||||||
this.labels = labels;
|
this.labels = labels != null ? labels.clone() : new String[0];
|
||||||
this.selected = selected;
|
this.selected = selected;
|
||||||
this.onSelect = onSelect;
|
this.onSelect = onSelect;
|
||||||
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||||
@@ -49,11 +54,36 @@ public class Segmented extends JComponent
|
|||||||
@Override
|
@Override
|
||||||
public void mousePressed(MouseEvent e)
|
public void mousePressed(MouseEvent e)
|
||||||
{
|
{
|
||||||
int i = indexAt(e.getX());
|
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
||||||
if (i >= 0)
|
|
||||||
{
|
{
|
||||||
setSelected(i);
|
int i = indexAt(e.getX());
|
||||||
onSelect.accept(i);
|
if (i >= 0 && onRightClick != null)
|
||||||
|
{
|
||||||
|
onRightClick.accept(i, e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SwingUtilities.isLeftMouseButton(e))
|
||||||
|
{
|
||||||
|
int i = indexAt(e.getX());
|
||||||
|
if (i >= 0)
|
||||||
|
{
|
||||||
|
setSelected(i);
|
||||||
|
onSelect.accept(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseReleased(MouseEvent e)
|
||||||
|
{
|
||||||
|
if (e.isPopupTrigger())
|
||||||
|
{
|
||||||
|
int i = indexAt(e.getX());
|
||||||
|
if (i >= 0 && onRightClick != null)
|
||||||
|
{
|
||||||
|
onRightClick.accept(i, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -61,7 +91,69 @@ public class Segmented extends JComponent
|
|||||||
addMouseMotionListener(mouse);
|
addMouseMotionListener(mouse);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Segmented ranges(TimeRange initial, java.util.function.Consumer<TimeRange> onSelect)
|
public void setOnRightClick(BiConsumer<Integer, MouseEvent> onRightClick)
|
||||||
|
{
|
||||||
|
this.onRightClick = onRightClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabels(String[] labels)
|
||||||
|
{
|
||||||
|
this.labels = labels != null ? labels.clone() : new String[0];
|
||||||
|
repaint();
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(int index, String label)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < labels.length)
|
||||||
|
{
|
||||||
|
labels[index] = label;
|
||||||
|
repaint();
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getLabels()
|
||||||
|
{
|
||||||
|
return labels.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Segmented ranges(TimeFilter filter, Consumer<TimeRange> onSelect)
|
||||||
|
{
|
||||||
|
List<TimeRange> slots = filter.getSlots();
|
||||||
|
String[] labels = new String[slots.size()];
|
||||||
|
for (int i = 0; i < slots.size(); i++)
|
||||||
|
{
|
||||||
|
labels[i] = slots.get(i).getLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
Segmented bar = new Segmented(labels, filter.getSlotIndex(filter.getRange()), i -> {
|
||||||
|
List<TimeRange> currentSlots = filter.getSlots();
|
||||||
|
if (i >= 0 && i < currentSlots.size())
|
||||||
|
{
|
||||||
|
onSelect.accept(currentSlots.get(i));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bar.setOnRightClick((slotIndex, mouseEvent) -> {
|
||||||
|
TimeRangeMenu.show(bar, filter, slotIndex, mouseEvent, onSelect);
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.addListener(() -> SwingUtilities.invokeLater(() -> {
|
||||||
|
List<TimeRange> currentSlots = filter.getSlots();
|
||||||
|
String[] currentLabels = new String[currentSlots.size()];
|
||||||
|
for (int i = 0; i < currentSlots.size(); i++)
|
||||||
|
{
|
||||||
|
currentLabels[i] = currentSlots.get(i).getLabel();
|
||||||
|
}
|
||||||
|
bar.setLabels(currentLabels);
|
||||||
|
bar.setSelected(filter.getSlotIndex(filter.getRange()));
|
||||||
|
}));
|
||||||
|
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Segmented ranges(TimeRange initial, Consumer<TimeRange> onSelect)
|
||||||
{
|
{
|
||||||
TimeRange[] values = TimeRange.values();
|
TimeRange[] values = TimeRange.values();
|
||||||
String[] labels = new String[values.length];
|
String[] labels = new String[values.length];
|
||||||
@@ -69,7 +161,7 @@ public class Segmented extends JComponent
|
|||||||
{
|
{
|
||||||
labels[i] = values[i].getLabel();
|
labels[i] = values[i].getLabel();
|
||||||
}
|
}
|
||||||
return new Segmented(labels, initial.ordinal(), i -> onSelect.accept(values[i]));
|
return new Segmented(labels, initial != null ? initial.ordinal() : 0, i -> onSelect.accept(values[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelected(int index)
|
public void setSelected(int index)
|
||||||
@@ -78,12 +170,32 @@ public class Segmented extends JComponent
|
|||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSelected()
|
||||||
|
{
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
private int indexAt(int x)
|
private int indexAt(int x)
|
||||||
{
|
{
|
||||||
|
if (labels == null || labels.length == 0)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
int i = x * labels.length / Math.max(1, getWidth());
|
int i = x * labels.length / Math.max(1, getWidth());
|
||||||
return i >= 0 && i < labels.length ? i : -1;
|
return i >= 0 && i < labels.length ? i : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getToolTipText(MouseEvent e)
|
||||||
|
{
|
||||||
|
int i = indexAt(e.getX());
|
||||||
|
if (i >= 0 && labels != null && i < labels.length)
|
||||||
|
{
|
||||||
|
return labels[i] + " (Right-click to customize)";
|
||||||
|
}
|
||||||
|
return super.getToolTipText(e);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Dimension getPreferredSize()
|
public Dimension getPreferredSize()
|
||||||
{
|
{
|
||||||
@@ -93,11 +205,15 @@ public class Segmented extends JComponent
|
|||||||
}
|
}
|
||||||
FontMetrics fm = getFontMetrics(FontManager.getRunescapeSmallFont());
|
FontMetrics fm = getFontMetrics(FontManager.getRunescapeSmallFont());
|
||||||
int w = 0;
|
int w = 0;
|
||||||
for (String l : labels)
|
if (labels != null)
|
||||||
{
|
{
|
||||||
w = Math.max(w, fm.stringWidth(l));
|
for (String l : labels)
|
||||||
|
{
|
||||||
|
w = Math.max(w, fm.stringWidth(l));
|
||||||
|
}
|
||||||
|
return new Dimension((w + 16) * labels.length, 24);
|
||||||
}
|
}
|
||||||
return new Dimension((w + 16) * labels.length, 24);
|
return new Dimension(100, 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -111,23 +227,26 @@ public class Segmented extends JComponent
|
|||||||
g.setFont(h >= 28 ? FontManager.getRunescapeBoldFont() : FontManager.getRunescapeSmallFont());
|
g.setFont(h >= 28 ? FontManager.getRunescapeBoldFont() : FontManager.getRunescapeSmallFont());
|
||||||
FontMetrics fm = g.getFontMetrics();
|
FontMetrics fm = g.getFontMetrics();
|
||||||
|
|
||||||
for (int i = 0; i < labels.length; i++)
|
if (labels != null)
|
||||||
{
|
{
|
||||||
int x0 = w * i / labels.length;
|
for (int i = 0; i < labels.length; i++)
|
||||||
int x1 = w * (i + 1) / labels.length;
|
|
||||||
boolean sel = i == selected;
|
|
||||||
if (sel)
|
|
||||||
{
|
{
|
||||||
g.setColor(Ui.ACCENT);
|
int x0 = w * i / labels.length;
|
||||||
g.fillRoundRect(x0 + 2, 2, x1 - x0 - 4, h - 4, 6, 6);
|
int x1 = w * (i + 1) / labels.length;
|
||||||
|
boolean sel = i == selected;
|
||||||
|
if (sel)
|
||||||
|
{
|
||||||
|
g.setColor(Ui.ACCENT);
|
||||||
|
g.fillRoundRect(x0 + 2, 2, x1 - x0 - 4, h - 4, 6, 6);
|
||||||
|
}
|
||||||
|
else if (i == hover)
|
||||||
|
{
|
||||||
|
g.setColor(Ui.CARD_HOVER.brighter());
|
||||||
|
g.fillRoundRect(x0 + 2, 2, x1 - x0 - 4, h - 4, 6, 6);
|
||||||
|
}
|
||||||
|
g.setColor(sel ? Color.WHITE : i == hover ? Ui.TEXT : Ui.MUTED);
|
||||||
|
g.drawString(labels[i], x0 + (x1 - x0 - fm.stringWidth(labels[i])) / 2, (h + fm.getAscent()) / 2 - 1);
|
||||||
}
|
}
|
||||||
else if (i == hover)
|
|
||||||
{
|
|
||||||
g.setColor(Ui.CARD_HOVER.brighter());
|
|
||||||
g.fillRoundRect(x0 + 2, 2, x1 - x0 - 4, h - 4, 6, 6);
|
|
||||||
}
|
|
||||||
g.setColor(sel ? Color.WHITE : i == hover ? Ui.TEXT : Ui.MUTED);
|
|
||||||
g.drawString(labels[i], x0 + (x1 - x0 - fm.stringWidth(labels[i])) / 2, (h + fm.getAscent()) / 2 - 1);
|
|
||||||
}
|
}
|
||||||
g.dispose();
|
g.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,25 @@ final class Series
|
|||||||
out.add(s);
|
out.add(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (before != null && (out.isEmpty() || out.get(0).t > start))
|
if (!out.isEmpty() && out.get(0).t > start)
|
||||||
|
{
|
||||||
|
// If the point before start was before a significant offline gap (> 10 mins),
|
||||||
|
// do not carry that old pre-logout value across the gap into this active session/range.
|
||||||
|
// Instead, the baseline at the start of this active period is its first active value.
|
||||||
|
if (before == null || (start - before.t > 10 * 60 * 1000L))
|
||||||
|
{
|
||||||
|
Snapshot first = new Snapshot(start, out.get(0).v);
|
||||||
|
first.m = out.get(0).m;
|
||||||
|
out.add(0, first);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snapshot first = new Snapshot(start, before.v);
|
||||||
|
first.m = before.m;
|
||||||
|
out.add(0, first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (before != null && out.isEmpty())
|
||||||
{
|
{
|
||||||
Snapshot first = new Snapshot(start, before.v);
|
Snapshot first = new Snapshot(start, before.v);
|
||||||
first.m = before.m;
|
first.m = before.m;
|
||||||
@@ -132,15 +150,15 @@ final class Series
|
|||||||
for (int i = 0; i < all.size(); i++)
|
for (int i = 0; i < all.size(); i++)
|
||||||
{
|
{
|
||||||
List<Snapshot> s = all.get(i);
|
List<Snapshot> s = all.get(i);
|
||||||
|
if (s.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
while (idx[i] + 1 < s.size() && s.get(idx[i] + 1).t <= t)
|
while (idx[i] + 1 < s.size() && s.get(idx[i] + 1).t <= t)
|
||||||
{
|
{
|
||||||
idx[i]++;
|
idx[i]++;
|
||||||
}
|
}
|
||||||
if (s.isEmpty() || s.get(idx[i]).t > t)
|
Snapshot p = s.get(idx[i]).t > t ? s.get(0) : s.get(idx[i]);
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Snapshot p = s.get(idx[i]);
|
|
||||||
for (int c = 0; c < v.length; c++)
|
for (int c = 0; c < v.length; c++)
|
||||||
{
|
{
|
||||||
v[c] += p.get(WealthCategory.values()[c]);
|
v[c] += p.get(WealthCategory.values()[c]);
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ public class SkillsPanel extends PluginPanel
|
|||||||
long[] rateStarts = h.rateStarts(timeFilter);
|
long[] rateStarts = h.rateStarts(timeFilter);
|
||||||
List<XpSample> samples = h.range(start);
|
List<XpSample> samples = h.range(start);
|
||||||
long chartStart = samples.isEmpty() ? now - Ui.HOUR : Math.max(start, samples.get(0).t);
|
long chartStart = samples.isEmpty() ? now - Ui.HOUR : Math.max(start, samples.get(0).t);
|
||||||
if (timeFilter.getRange() != TimeRange.ALL)
|
if (timeFilter.getRange() != null && !timeFilter.getRange().isAll())
|
||||||
{
|
{
|
||||||
chartStart = start;
|
chartStart = start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.example.essentialsplus.ui;
|
package com.example.essentialsplus.ui;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -14,29 +16,144 @@ public final class TimeFilter
|
|||||||
/** login time of the current game session (hopping keeps it) */
|
/** login time of the current game session (hopping keeps it) */
|
||||||
private volatile long sessionStart = System.currentTimeMillis();
|
private volatile long sessionStart = System.currentTimeMillis();
|
||||||
|
|
||||||
|
public static final TimeRange[] DEFAULT_SLOTS = {
|
||||||
|
TimeRange.CURRENT,
|
||||||
|
TimeRange.SESSION,
|
||||||
|
TimeRange.HOUR,
|
||||||
|
TimeRange.DAY,
|
||||||
|
TimeRange.WEEK,
|
||||||
|
TimeRange.MONTH,
|
||||||
|
TimeRange.ALL
|
||||||
|
};
|
||||||
|
|
||||||
|
private final List<TimeRange> slots = new CopyOnWriteArrayList<>();
|
||||||
|
private final Consumer<String> onPersistSlots;
|
||||||
|
|
||||||
private volatile TimeRange range;
|
private volatile TimeRange range;
|
||||||
private final Consumer<TimeRange> onPersist;
|
private final Consumer<TimeRange> onPersist;
|
||||||
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
|
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
public TimeFilter(TimeRange initial, Consumer<TimeRange> onPersist)
|
public TimeFilter(TimeRange initial, Consumer<TimeRange> onPersist)
|
||||||
{
|
{
|
||||||
this.range = initial == null ? TimeRange.DAY : initial;
|
this(initial, onPersist, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeFilter(TimeRange initial, Consumer<TimeRange> onPersist, String savedSlots, Consumer<String> onPersistSlots)
|
||||||
|
{
|
||||||
this.onPersist = onPersist;
|
this.onPersist = onPersist;
|
||||||
|
this.onPersistSlots = onPersistSlots;
|
||||||
|
initSlots(savedSlots);
|
||||||
|
this.range = initial == null ? TimeRange.DAY : initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSlots(String saved)
|
||||||
|
{
|
||||||
|
slots.clear();
|
||||||
|
if (saved != null && !saved.trim().isEmpty())
|
||||||
|
{
|
||||||
|
String[] parts = saved.split(",");
|
||||||
|
for (String part : parts)
|
||||||
|
{
|
||||||
|
TimeRange r = parse(part.trim());
|
||||||
|
if (r != null)
|
||||||
|
{
|
||||||
|
slots.add(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slots.size() != DEFAULT_SLOTS.length)
|
||||||
|
{
|
||||||
|
slots.clear();
|
||||||
|
for (TimeRange def : DEFAULT_SLOTS)
|
||||||
|
{
|
||||||
|
slots.add(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TimeRange> getSlots()
|
||||||
|
{
|
||||||
|
return Collections.unmodifiableList(new ArrayList<>(slots));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSlotIndex(TimeRange r)
|
||||||
|
{
|
||||||
|
if (r == null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < slots.size(); i++)
|
||||||
|
{
|
||||||
|
if (r.equals(slots.get(i)))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSlot(int index, TimeRange newRange)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= slots.size() || newRange == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slots.set(index, newRange);
|
||||||
|
persistSlots();
|
||||||
|
for (Runnable l : listeners)
|
||||||
|
{
|
||||||
|
l.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetSlot(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= slots.size())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slots.set(index, DEFAULT_SLOTS[index]);
|
||||||
|
persistSlots();
|
||||||
|
for (Runnable l : listeners)
|
||||||
|
{
|
||||||
|
l.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetSlots()
|
||||||
|
{
|
||||||
|
slots.clear();
|
||||||
|
for (TimeRange def : DEFAULT_SLOTS)
|
||||||
|
{
|
||||||
|
slots.add(def);
|
||||||
|
}
|
||||||
|
persistSlots();
|
||||||
|
for (Runnable l : listeners)
|
||||||
|
{
|
||||||
|
l.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistSlots()
|
||||||
|
{
|
||||||
|
if (onPersistSlots != null)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < slots.size(); i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
sb.append(",");
|
||||||
|
}
|
||||||
|
sb.append(slots.get(i).name());
|
||||||
|
}
|
||||||
|
onPersistSlots.accept(sb.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TimeRange parse(String name)
|
public static TimeRange parse(String name)
|
||||||
{
|
{
|
||||||
if (name != null)
|
return TimeRange.parse(name);
|
||||||
{
|
|
||||||
for (TimeRange r : TimeRange.values())
|
|
||||||
{
|
|
||||||
if (r.name().equals(name))
|
|
||||||
{
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return TimeRange.DAY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimeRange getRange()
|
public TimeRange getRange()
|
||||||
@@ -46,23 +163,25 @@ public final class TimeFilter
|
|||||||
|
|
||||||
public String getLabel()
|
public String getLabel()
|
||||||
{
|
{
|
||||||
return range.getLabel();
|
return range != null ? range.getLabel() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Label for "nothing in ..." texts. */
|
/** Label for "nothing in ..." texts. */
|
||||||
public String inLabel()
|
public String inLabel()
|
||||||
{
|
{
|
||||||
switch (range)
|
if (range == null || range.isAll())
|
||||||
{
|
{
|
||||||
case CURRENT:
|
return "total";
|
||||||
return "right now";
|
|
||||||
case SESSION:
|
|
||||||
return "this session";
|
|
||||||
case ALL:
|
|
||||||
return "total";
|
|
||||||
default:
|
|
||||||
return "in " + range.getLabel();
|
|
||||||
}
|
}
|
||||||
|
if (range.isCurrent())
|
||||||
|
{
|
||||||
|
return "right now";
|
||||||
|
}
|
||||||
|
if (range.isSession())
|
||||||
|
{
|
||||||
|
return "this session";
|
||||||
|
}
|
||||||
|
return "in " + range.getLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getSessionStart()
|
public long getSessionStart()
|
||||||
@@ -82,7 +201,7 @@ public final class TimeFilter
|
|||||||
|
|
||||||
public boolean isCurrent()
|
public boolean isCurrent()
|
||||||
{
|
{
|
||||||
return range == TimeRange.CURRENT;
|
return range != null && range.isCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,17 +211,19 @@ public final class TimeFilter
|
|||||||
public long start()
|
public long start()
|
||||||
{
|
{
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
switch (range)
|
if (range == null || range.isAll())
|
||||||
{
|
{
|
||||||
case ALL:
|
return 0;
|
||||||
return 0;
|
|
||||||
case SESSION:
|
|
||||||
return sessionStart;
|
|
||||||
case CURRENT:
|
|
||||||
return now - ACTIVE_GAP;
|
|
||||||
default:
|
|
||||||
return now - range.getMillis();
|
|
||||||
}
|
}
|
||||||
|
if (range.isSession())
|
||||||
|
{
|
||||||
|
return sessionStart;
|
||||||
|
}
|
||||||
|
if (range.isCurrent())
|
||||||
|
{
|
||||||
|
return now - ACTIVE_GAP;
|
||||||
|
}
|
||||||
|
return now - range.getMillis();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,7 +265,7 @@ public final class TimeFilter
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean changed = r != range;
|
boolean changed = !r.equals(range);
|
||||||
range = r;
|
range = r;
|
||||||
if (changed && onPersist != null)
|
if (changed && onPersist != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,22 +1,288 @@
|
|||||||
package com.example.essentialsplus.ui;
|
package com.example.essentialsplus.ui;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
public final class TimeRange
|
||||||
public enum TimeRange
|
|
||||||
{
|
{
|
||||||
/** only what is being gained right now: resets after {@link TimeFilter#ACTIVE_GAP} without progress */
|
/** only what is being gained right now: resets after {@link TimeFilter#ACTIVE_GAP} without progress */
|
||||||
CURRENT("Now", -1),
|
public static final TimeRange CURRENT = new TimeRange("CURRENT", "Now", -1);
|
||||||
/** since the client was started */
|
/** since the client was started */
|
||||||
SESSION("Sess", -1),
|
public static final TimeRange SESSION = new TimeRange("SESSION", "Sess", -1);
|
||||||
HOUR("1H", Ui.HOUR),
|
public static final TimeRange HOUR = new TimeRange("HOUR", "1H", Ui.HOUR);
|
||||||
DAY("1D", Ui.DAY),
|
public static final TimeRange DAY = new TimeRange("DAY", "1D", Ui.DAY);
|
||||||
TWO_WEEKS("2W", 14 * Ui.DAY),
|
public static final TimeRange WEEK = new TimeRange("WEEK", "1W", 7 * Ui.DAY);
|
||||||
QUARTER("3M", 91 * Ui.DAY),
|
public static final TimeRange MONTH = new TimeRange("MONTH", "1M", 30 * Ui.DAY);
|
||||||
ALL("All", -1);
|
public static final TimeRange ALL = new TimeRange("ALL", "All", -1);
|
||||||
|
|
||||||
|
public static final TimeRange[] DEFAULT_VALUES = {
|
||||||
|
CURRENT, SESSION, HOUR, DAY, WEEK, MONTH, ALL
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final Pattern PATTERN = Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)\\s*([a-zA-Z]+)$");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
private final String label;
|
private final String label;
|
||||||
private final long millis;
|
private final long millis;
|
||||||
|
|
||||||
|
public TimeRange(String id, String label, long millis)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.label = label;
|
||||||
|
this.millis = millis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeRange[] values()
|
||||||
|
{
|
||||||
|
return DEFAULT_VALUES.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeRange valueOf(String name)
|
||||||
|
{
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
throw new NullPointerException("Name is null");
|
||||||
|
}
|
||||||
|
for (TimeRange r : DEFAULT_VALUES)
|
||||||
|
{
|
||||||
|
if (r.name().equals(name))
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("No constant " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name()
|
||||||
|
{
|
||||||
|
return id != null ? id : (millis > 0 ? "CUSTOM:" + label + ":" + millis : label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ordinal()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < DEFAULT_VALUES.length; i++)
|
||||||
|
{
|
||||||
|
if (this.equals(DEFAULT_VALUES[i]))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCurrent()
|
||||||
|
{
|
||||||
|
return this == CURRENT || "CURRENT".equals(id) || "Now".equals(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSession()
|
||||||
|
{
|
||||||
|
return this == SESSION || "SESSION".equals(id) || "Sess".equals(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAll()
|
||||||
|
{
|
||||||
|
return this == ALL || "ALL".equals(id) || "All".equals(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeRange custom(String label, long millis)
|
||||||
|
{
|
||||||
|
for (TimeRange def : DEFAULT_VALUES)
|
||||||
|
{
|
||||||
|
if (def.millis == millis && def.label.equalsIgnoreCase(label))
|
||||||
|
{
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new TimeRange("CUSTOM:" + label + ":" + millis, label, millis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeRange parseCustom(String input)
|
||||||
|
{
|
||||||
|
if (input == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String s = input.trim();
|
||||||
|
if (s.isEmpty())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.equalsIgnoreCase("now"))
|
||||||
|
{
|
||||||
|
return CURRENT;
|
||||||
|
}
|
||||||
|
if (s.equalsIgnoreCase("sess") || s.equalsIgnoreCase("session"))
|
||||||
|
{
|
||||||
|
return SESSION;
|
||||||
|
}
|
||||||
|
if (s.equalsIgnoreCase("all") || s.equalsIgnoreCase("all time"))
|
||||||
|
{
|
||||||
|
return ALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher m = PATTERN.matcher(s);
|
||||||
|
if (!m.matches())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
double val = Double.parseDouble(m.group(1));
|
||||||
|
if (val <= 0 || Double.isNaN(val) || Double.isInfinite(val))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String rawUnit = m.group(2);
|
||||||
|
String unit = rawUnit.toLowerCase(Locale.ROOT);
|
||||||
|
long millis;
|
||||||
|
String label;
|
||||||
|
String numStr = (val == (long) val) ? String.valueOf((long) val) : String.format(Locale.ROOT, "%.1f", val);
|
||||||
|
|
||||||
|
if (unit.equals("s") || unit.startsWith("sec"))
|
||||||
|
{
|
||||||
|
millis = (long) (val * 1000L);
|
||||||
|
label = numStr + "s";
|
||||||
|
}
|
||||||
|
else if (unit.equals("min") || unit.startsWith("minute"))
|
||||||
|
{
|
||||||
|
millis = (long) (val * 60 * 1000L);
|
||||||
|
label = numStr + "m";
|
||||||
|
}
|
||||||
|
else if (unit.equals("h") || unit.startsWith("hour") || unit.startsWith("hr"))
|
||||||
|
{
|
||||||
|
millis = (long) (val * Ui.HOUR);
|
||||||
|
label = numStr + "H";
|
||||||
|
}
|
||||||
|
else if (unit.equals("d") || unit.startsWith("day"))
|
||||||
|
{
|
||||||
|
millis = (long) (val * Ui.DAY);
|
||||||
|
label = numStr + "D";
|
||||||
|
}
|
||||||
|
else if (unit.equals("w") || unit.startsWith("week") || unit.startsWith("wk"))
|
||||||
|
{
|
||||||
|
millis = (long) (val * 7 * Ui.DAY);
|
||||||
|
label = numStr + "W";
|
||||||
|
}
|
||||||
|
else if (unit.equals("mo") || unit.startsWith("mon") || unit.startsWith("month") || rawUnit.equals("M"))
|
||||||
|
{
|
||||||
|
millis = (long) (val * 30 * Ui.DAY);
|
||||||
|
label = numStr + "M";
|
||||||
|
}
|
||||||
|
else if (unit.equals("y") || unit.startsWith("year") || unit.startsWith("yr"))
|
||||||
|
{
|
||||||
|
millis = (long) (val * 365 * Ui.DAY);
|
||||||
|
label = numStr + "Y";
|
||||||
|
}
|
||||||
|
else if (unit.equals("m"))
|
||||||
|
{
|
||||||
|
if (val >= 15)
|
||||||
|
{
|
||||||
|
millis = (long) (val * 60 * 1000L);
|
||||||
|
label = numStr + "m";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
millis = (long) (val * 30 * Ui.DAY);
|
||||||
|
label = numStr + "M";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return custom(label, millis);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeRange parse(String name)
|
||||||
|
{
|
||||||
|
if (name == null || name.trim().isEmpty())
|
||||||
|
{
|
||||||
|
return TimeRange.DAY;
|
||||||
|
}
|
||||||
|
name = name.trim();
|
||||||
|
if ("TWO_WEEKS".equals(name))
|
||||||
|
{
|
||||||
|
return TimeRange.WEEK;
|
||||||
|
}
|
||||||
|
if ("QUARTER".equals(name))
|
||||||
|
{
|
||||||
|
return TimeRange.MONTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TimeRange r : DEFAULT_VALUES)
|
||||||
|
{
|
||||||
|
if (r.name().equalsIgnoreCase(name) || r.getLabel().equalsIgnoreCase(name))
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.startsWith("CUSTOM:"))
|
||||||
|
{
|
||||||
|
String[] p = name.split(":", 3);
|
||||||
|
if (p.length == 3)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new TimeRange(name, p[1], Long.parseLong(p[2]));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeRange custom = parseCustom(name);
|
||||||
|
if (custom != null)
|
||||||
|
{
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeRange.DAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (this == o)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TimeRange that = (TimeRange) o;
|
||||||
|
return millis == that.millis && Objects.equals(label, that.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hash(label, millis);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.example.essentialsplus.ui;
|
||||||
|
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import javax.swing.JMenu;
|
||||||
|
import javax.swing.JMenuItem;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPopupMenu;
|
||||||
|
import net.runelite.client.ui.FontManager;
|
||||||
|
|
||||||
|
public final class TimeRangeMenu
|
||||||
|
{
|
||||||
|
private TimeRangeMenu()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(Component parent, TimeFilter filter, int slotIndex, MouseEvent e, Consumer<TimeRange> onSelect)
|
||||||
|
{
|
||||||
|
List<TimeRange> slots = filter.getSlots();
|
||||||
|
if (slotIndex < 0 || slotIndex >= slots.size())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeRange current = slots.get(slotIndex);
|
||||||
|
TimeRange defaultRange = TimeFilter.DEFAULT_SLOTS[slotIndex];
|
||||||
|
|
||||||
|
JPopupMenu menu = new JPopupMenu();
|
||||||
|
|
||||||
|
JMenuItem title = new JMenuItem("Slot " + (slotIndex + 1) + " (" + current.getLabel() + ")");
|
||||||
|
title.setFont(FontManager.getRunescapeBoldFont());
|
||||||
|
title.setEnabled(false);
|
||||||
|
menu.add(title);
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// 1. Minutes
|
||||||
|
JMenu minutesMenu = new JMenu("Minutes");
|
||||||
|
addPreset(minutesMenu, filter, slotIndex, onSelect, "5m", 5 * 60 * 1000L);
|
||||||
|
addPreset(minutesMenu, filter, slotIndex, onSelect, "10m", 10 * 60 * 1000L);
|
||||||
|
addPreset(minutesMenu, filter, slotIndex, onSelect, "15m", 15 * 60 * 1000L);
|
||||||
|
addPreset(minutesMenu, filter, slotIndex, onSelect, "30m", 30 * 60 * 1000L);
|
||||||
|
addPreset(minutesMenu, filter, slotIndex, onSelect, "45m", 45 * 60 * 1000L);
|
||||||
|
menu.add(minutesMenu);
|
||||||
|
|
||||||
|
// 2. Hours
|
||||||
|
JMenu hoursMenu = new JMenu("Hours");
|
||||||
|
addPreset(hoursMenu, filter, slotIndex, onSelect, "1H", Ui.HOUR);
|
||||||
|
addPreset(hoursMenu, filter, slotIndex, onSelect, "2H", 2 * Ui.HOUR);
|
||||||
|
addPreset(hoursMenu, filter, slotIndex, onSelect, "3H", 3 * Ui.HOUR);
|
||||||
|
addPreset(hoursMenu, filter, slotIndex, onSelect, "4H", 4 * Ui.HOUR);
|
||||||
|
addPreset(hoursMenu, filter, slotIndex, onSelect, "6H", 6 * Ui.HOUR);
|
||||||
|
addPreset(hoursMenu, filter, slotIndex, onSelect, "8H", 8 * Ui.HOUR);
|
||||||
|
addPreset(hoursMenu, filter, slotIndex, onSelect, "12H", 12 * Ui.HOUR);
|
||||||
|
menu.add(hoursMenu);
|
||||||
|
|
||||||
|
// 3. Days
|
||||||
|
JMenu daysMenu = new JMenu("Days");
|
||||||
|
addPreset(daysMenu, filter, slotIndex, onSelect, "1D", Ui.DAY);
|
||||||
|
addPreset(daysMenu, filter, slotIndex, onSelect, "2D", 2 * Ui.DAY);
|
||||||
|
addPreset(daysMenu, filter, slotIndex, onSelect, "3D", 3 * Ui.DAY);
|
||||||
|
addPreset(daysMenu, filter, slotIndex, onSelect, "4D", 4 * Ui.DAY);
|
||||||
|
addPreset(daysMenu, filter, slotIndex, onSelect, "5D", 5 * Ui.DAY);
|
||||||
|
addPreset(daysMenu, filter, slotIndex, onSelect, "6D", 6 * Ui.DAY);
|
||||||
|
addPreset(daysMenu, filter, slotIndex, onSelect, "7D", 7 * Ui.DAY);
|
||||||
|
menu.add(daysMenu);
|
||||||
|
|
||||||
|
// 4. Weeks
|
||||||
|
JMenu weeksMenu = new JMenu("Weeks");
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "1W", 7 * Ui.DAY);
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "2W", 14 * Ui.DAY);
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "3W", 21 * Ui.DAY);
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "4W", 28 * Ui.DAY);
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "5W", 35 * Ui.DAY);
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "6W", 42 * Ui.DAY);
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "8W", 56 * Ui.DAY);
|
||||||
|
addPreset(weeksMenu, filter, slotIndex, onSelect, "12W", 84 * Ui.DAY);
|
||||||
|
menu.add(weeksMenu);
|
||||||
|
|
||||||
|
// 5. Months
|
||||||
|
JMenu monthsMenu = new JMenu("Months");
|
||||||
|
addPreset(monthsMenu, filter, slotIndex, onSelect, "1M", 30 * Ui.DAY);
|
||||||
|
addPreset(monthsMenu, filter, slotIndex, onSelect, "2M", 60 * Ui.DAY);
|
||||||
|
addPreset(monthsMenu, filter, slotIndex, onSelect, "3M", 90 * Ui.DAY);
|
||||||
|
addPreset(monthsMenu, filter, slotIndex, onSelect, "4M", 120 * Ui.DAY);
|
||||||
|
addPreset(monthsMenu, filter, slotIndex, onSelect, "6M", 180 * Ui.DAY);
|
||||||
|
addPreset(monthsMenu, filter, slotIndex, onSelect, "9M", 270 * Ui.DAY);
|
||||||
|
addPreset(monthsMenu, filter, slotIndex, onSelect, "12M", 360 * Ui.DAY);
|
||||||
|
menu.add(monthsMenu);
|
||||||
|
|
||||||
|
// 6. Years
|
||||||
|
JMenu yearsMenu = new JMenu("Years");
|
||||||
|
addPreset(yearsMenu, filter, slotIndex, onSelect, "1Y", 365 * Ui.DAY);
|
||||||
|
addPreset(yearsMenu, filter, slotIndex, onSelect, "2Y", 2 * 365 * Ui.DAY);
|
||||||
|
addPreset(yearsMenu, filter, slotIndex, onSelect, "3Y", 3 * 365 * Ui.DAY);
|
||||||
|
addPreset(yearsMenu, filter, slotIndex, onSelect, "5Y", 5 * 365 * Ui.DAY);
|
||||||
|
menu.add(yearsMenu);
|
||||||
|
|
||||||
|
// 7. Special
|
||||||
|
JMenu specialMenu = new JMenu("Special");
|
||||||
|
addSpecial(specialMenu, filter, slotIndex, onSelect, TimeRange.CURRENT, "Now (Live streak)");
|
||||||
|
addSpecial(specialMenu, filter, slotIndex, onSelect, TimeRange.SESSION, "Sess (Session)");
|
||||||
|
addSpecial(specialMenu, filter, slotIndex, onSelect, TimeRange.ALL, "All (All time)");
|
||||||
|
menu.add(specialMenu);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// 8. Custom...
|
||||||
|
JMenuItem customItem = new JMenuItem("Custom...");
|
||||||
|
customItem.addActionListener(a -> {
|
||||||
|
String input = JOptionPane.showInputDialog(
|
||||||
|
parent,
|
||||||
|
"Enter custom time range:\n(e.g., 30m, 2h, 3d, 2w, 5w, 6mo, 1y)",
|
||||||
|
"Custom Time Range for Slot " + (slotIndex + 1),
|
||||||
|
JOptionPane.QUESTION_MESSAGE
|
||||||
|
);
|
||||||
|
if (input != null && !input.trim().isEmpty())
|
||||||
|
{
|
||||||
|
TimeRange parsed = TimeRange.parseCustom(input.trim());
|
||||||
|
if (parsed != null)
|
||||||
|
{
|
||||||
|
applyRange(filter, slotIndex, onSelect, parsed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JOptionPane.showMessageDialog(
|
||||||
|
parent,
|
||||||
|
"Could not parse \"" + input.trim() + "\".\n\nValid examples:\n• Minutes: 15m, 30min\n• Hours: 2h, 36h\n• Days: 3d, 10d\n• Weeks: 2w, 5w\n• Months: 2mo, 6m\n• Years: 1y, 2y\n• Special: now, sess, all",
|
||||||
|
"Invalid Format",
|
||||||
|
JOptionPane.ERROR_MESSAGE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.add(customItem);
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
|
// 9. Reset this slot
|
||||||
|
JMenuItem resetSlot = new JMenuItem("Reset Slot " + (slotIndex + 1) + " (Default: " + defaultRange.getLabel() + ")");
|
||||||
|
resetSlot.addActionListener(a -> {
|
||||||
|
filter.resetSlot(slotIndex);
|
||||||
|
if (onSelect != null)
|
||||||
|
{
|
||||||
|
onSelect.accept(defaultRange);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.add(resetSlot);
|
||||||
|
|
||||||
|
// 10. Reset all slots
|
||||||
|
JMenuItem resetAll = new JMenuItem("Reset All Slots to Default");
|
||||||
|
resetAll.addActionListener(a -> {
|
||||||
|
filter.resetSlots();
|
||||||
|
if (onSelect != null)
|
||||||
|
{
|
||||||
|
onSelect.accept(defaultRange);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.add(resetAll);
|
||||||
|
|
||||||
|
menu.show(parent, e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPreset(JMenu parent, TimeFilter filter, int slotIndex, Consumer<TimeRange> onSelect, String label, long millis)
|
||||||
|
{
|
||||||
|
JMenuItem item = new JMenuItem(label);
|
||||||
|
item.addActionListener(a -> {
|
||||||
|
TimeRange r = TimeRange.custom(label, millis);
|
||||||
|
applyRange(filter, slotIndex, onSelect, r);
|
||||||
|
});
|
||||||
|
parent.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addSpecial(JMenu parent, TimeFilter filter, int slotIndex, Consumer<TimeRange> onSelect, TimeRange range, String desc)
|
||||||
|
{
|
||||||
|
JMenuItem item = new JMenuItem(desc);
|
||||||
|
item.addActionListener(a -> applyRange(filter, slotIndex, onSelect, range));
|
||||||
|
parent.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyRange(TimeFilter filter, int slotIndex, Consumer<TimeRange> onSelect, TimeRange range)
|
||||||
|
{
|
||||||
|
filter.setSlot(slotIndex, range);
|
||||||
|
if (onSelect != null)
|
||||||
|
{
|
||||||
|
onSelect.accept(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -220,7 +220,7 @@ public class WealthPanel extends PluginPanel
|
|||||||
sessionView = false;
|
sessionView = false;
|
||||||
if (popout != null)
|
if (popout != null)
|
||||||
{
|
{
|
||||||
popout.setRange(timeFilter.getRange().ordinal());
|
popout.setRange(timeFilter.getSlotIndex(timeFilter.getRange()));
|
||||||
}
|
}
|
||||||
refreshLater();
|
refreshLater();
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@ public class WealthPanel extends PluginPanel
|
|||||||
|
|
||||||
public boolean isSessionView()
|
public boolean isSessionView()
|
||||||
{
|
{
|
||||||
return sessionView || (timeFilter != null && timeFilter.getRange() == TimeRange.SESSION);
|
return sessionView || (timeFilter != null && timeFilter.getRange() != null && timeFilter.getRange().isSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isHourly()
|
public boolean isHourly()
|
||||||
@@ -329,6 +329,11 @@ public class WealthPanel extends PluginPanel
|
|||||||
plugin.resetWealthHourly(key);
|
plugin.resetWealthHourly(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TimeFilter getTimeFilter()
|
||||||
|
{
|
||||||
|
return timeFilter;
|
||||||
|
}
|
||||||
|
|
||||||
TimeRange getRange()
|
TimeRange getRange()
|
||||||
{
|
{
|
||||||
return timeFilter.getRange();
|
return timeFilter.getRange();
|
||||||
@@ -482,8 +487,8 @@ public class WealthPanel extends PluginPanel
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
start = range == TimeRange.ALL ? Series.earliest(accounts) : timeFilter.start();
|
start = (range != null && range.isAll()) ? Series.earliest(accounts) : timeFilter.start();
|
||||||
rangeLabel = range.getLabel();
|
rangeLabel = range != null ? range.getLabel() : "total";
|
||||||
}
|
}
|
||||||
sessionLabel.setText(sessionStart <= 0 ? "No session started"
|
sessionLabel.setText(sessionStart <= 0 ? "No session started"
|
||||||
: "Session since " + Instant.ofEpochMilli(sessionStart).atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("d MMM HH:mm", Locale.ENGLISH))
|
: "Session since " + Instant.ofEpochMilli(sessionStart).atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("d MMM HH:mm", Locale.ENGLISH))
|
||||||
@@ -513,28 +518,43 @@ public class WealthPanel extends PluginPanel
|
|||||||
List<List<Snapshot>> allSeries = new ArrayList<>();
|
List<List<Snapshot>> allSeries = new ArrayList<>();
|
||||||
long currentStart = now;
|
long currentStart = now;
|
||||||
// price movements of held items only count as profit/loss over all time
|
// price movements of held items only count as profit/loss over all time
|
||||||
boolean ignorePrices = isSession || range != TimeRange.ALL;
|
boolean ignorePrices = isSession || (range != null && !range.isAll());
|
||||||
boolean isHourly = isHourly();
|
boolean isHourly = isHourly();
|
||||||
int needBank = 0;
|
int needBank = 0;
|
||||||
long totalNow = 0;
|
long totalNow = 0;
|
||||||
Map<String, String> charGphMap = new HashMap<>();
|
Map<String, String> charGphMap = new HashMap<>();
|
||||||
|
|
||||||
|
if (timeFilter.isCurrent() && !isSession)
|
||||||
|
{
|
||||||
|
long activeStart = Long.MAX_VALUE;
|
||||||
|
for (AccountData a : accounts)
|
||||||
|
{
|
||||||
|
boolean live = a.key.equals(active);
|
||||||
|
if (live || now - a.lastUpdated <= TimeFilter.ACTIVE_GAP)
|
||||||
|
{
|
||||||
|
activeStart = Math.min(activeStart, computeActiveStreakStart(a.history, a.lastUpdated, now, live));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentStart = activeStart == Long.MAX_VALUE ? now - TimeFilter.ACTIVE_GAP : activeStart;
|
||||||
|
start = currentStart;
|
||||||
|
}
|
||||||
|
|
||||||
for (AccountData a : accounts)
|
for (AccountData a : accounts)
|
||||||
{
|
{
|
||||||
for (TopItem t : a.topItems)
|
for (TopItem t : a.topItems)
|
||||||
{
|
{
|
||||||
icons.putName(t.id, t.name);
|
icons.putName(t.id, t.name);
|
||||||
}
|
}
|
||||||
|
boolean live = a.key.equals(active);
|
||||||
long accountStart = start;
|
long accountStart = start;
|
||||||
if (timeFilter.isCurrent() && !isSession)
|
if (timeFilter.isCurrent() && !isSession)
|
||||||
{
|
{
|
||||||
List<Snapshot> h = a.history;
|
accountStart = computeActiveStreakStart(a.history, a.lastUpdated, now, live);
|
||||||
accountStart = computeActiveStreakStart(h, a.lastUpdated, now);
|
|
||||||
}
|
}
|
||||||
currentStart = Math.min(currentStart, accountStart);
|
List<Snapshot> sCombined = Series.forAccount(a, start, now, ignorePrices);
|
||||||
List<Snapshot> s = Series.forAccount(a, accountStart, now, ignorePrices);
|
allSeries.add(sCombined);
|
||||||
allSeries.add(s);
|
List<Snapshot> s = (accountStart == start) ? sCombined : Series.forAccount(a, accountStart, now, ignorePrices);
|
||||||
long delta = s.size() > 1 ? s.get(s.size() - 1).total() - s.get(0).total() : 0;
|
long delta = s.size() > 1 ? s.get(s.size() - 1).total() - s.get(0).total() : 0;
|
||||||
boolean live = a.key.equals(active);
|
|
||||||
|
|
||||||
String status;
|
String status;
|
||||||
java.awt.Color statusColor;
|
java.awt.Color statusColor;
|
||||||
@@ -690,7 +710,7 @@ public class WealthPanel extends PluginPanel
|
|||||||
Model m = new Model();
|
Model m = new Model();
|
||||||
m.hourlyRate = hourlyRate;
|
m.hourlyRate = hourlyRate;
|
||||||
m.start = start;
|
m.start = start;
|
||||||
m.ignorePrices = isSessionView() || timeFilter.getRange() != TimeRange.ALL;
|
m.ignorePrices = isSessionView() || (timeFilter.getRange() != null && !timeFilter.getRange().isAll());
|
||||||
m.end = now;
|
m.end = now;
|
||||||
m.ledgers = new ArrayList<>();
|
m.ledgers = new ArrayList<>();
|
||||||
m.selection = selection;
|
m.selection = selection;
|
||||||
@@ -1029,7 +1049,7 @@ public class WealthPanel extends PluginPanel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long computeActiveStreakStart(List<Snapshot> h, long lastUpdated, long now)
|
private static long computeActiveStreakStart(List<Snapshot> h, long lastUpdated, long now, boolean isActive)
|
||||||
{
|
{
|
||||||
if (h == null || h.isEmpty())
|
if (h == null || h.isEmpty())
|
||||||
{
|
{
|
||||||
@@ -1037,7 +1057,7 @@ public class WealthPanel extends PluginPanel
|
|||||||
}
|
}
|
||||||
|
|
||||||
long latest = Math.max(lastUpdated, h.get(h.size() - 1).t);
|
long latest = Math.max(lastUpdated, h.get(h.size() - 1).t);
|
||||||
if (now - latest > TimeFilter.ACTIVE_GAP)
|
if (now - latest > TimeFilter.ACTIVE_GAP || (!isActive && now - lastUpdated > TimeFilter.ACTIVE_GAP))
|
||||||
{
|
{
|
||||||
// Character has been idle / logged out for more than 5 minutes
|
// Character has been idle / logged out for more than 5 minutes
|
||||||
return now - TimeFilter.ACTIVE_GAP;
|
return now - TimeFilter.ACTIVE_GAP;
|
||||||
@@ -1060,11 +1080,11 @@ public class WealthPanel extends PluginPanel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Baseline is right before the first active point of this session
|
// Baseline is right before the first active point of this session, only if part of the continuous streak
|
||||||
if (firstIdx > 0)
|
if (firstIdx > 0 && streakStart - h.get(firstIdx - 1).t <= 10 * Ui.MINUTE)
|
||||||
{
|
{
|
||||||
return Math.max(h.get(firstIdx - 1).t, streakStart - Ui.MINUTE);
|
return Math.max(h.get(firstIdx - 1).t, streakStart - Ui.MINUTE);
|
||||||
}
|
}
|
||||||
return streakStart - Ui.MINUTE;
|
return streakStart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ class WealthPopout extends JFrame
|
|||||||
view = new WealthPanel.DetailView(panel.getIcons(), true);
|
view = new WealthPanel.DetailView(panel.getIcons(), true);
|
||||||
view.main.setMode(panel.getMainMode());
|
view.main.setMode(panel.getMainMode());
|
||||||
view.breakdown.setOnOpen((v, label) -> panel.openItems(v, label, resolved));
|
view.breakdown.setOnOpen((v, label) -> panel.openItems(v, label, resolved));
|
||||||
rangeBar = Segmented.ranges(range, panel::setRange);
|
rangeBar = Segmented.ranges(panel.getTimeFilter(), panel::setRange);
|
||||||
rangeBar.setSelected(panel.isCustomSessionView() ? -1 : range.ordinal());
|
rangeBar.setSelected(panel.isCustomSessionView() ? -1 : panel.getTimeFilter().getSlotIndex(range));
|
||||||
styleBar = new Segmented(new String[]{"Line", "Candles"}, panel.getMainMode() == WealthChart.Mode.CANDLES ? 1 : 0,
|
styleBar = new Segmented(new String[]{"Line", "Candles"}, panel.getMainMode() == WealthChart.Mode.CANDLES ? 1 : 0,
|
||||||
i -> panel.setMainMode(i == 1 ? WealthChart.Mode.CANDLES : WealthChart.Mode.LINE));
|
i -> panel.setMainMode(i == 1 ? WealthChart.Mode.CANDLES : WealthChart.Mode.LINE));
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ final class WikiHtml
|
|||||||
/** Kept as they are (with filtered attributes). */
|
/** Kept as they are (with filtered attributes). */
|
||||||
private static final Set<String> KEEP = Set.of("a", "b", "strong", "i", "em", "u", "s", "strike", "p", "br", "hr",
|
private static final Set<String> KEEP = Set.of("a", "b", "strong", "i", "em", "u", "s", "strike", "p", "br", "hr",
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "dt", "dd", "table", "tr", "td", "th", "caption", "img",
|
"h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "dt", "dd", "table", "tr", "td", "th", "caption", "img",
|
||||||
"div", "span", "pre", "code", "sup", "sub", "center", "blockquote", "small", "big", "tt", "kbd", "var", "cite");
|
"div", "span", "pre", "code", "sup", "sub", "center", "blockquote", "small", "big", "tt", "kbd", "var", "cite", "font");
|
||||||
/** Unknown to Swing, rendered as another tag. */
|
/** Unknown to Swing, rendered as another tag. */
|
||||||
private static final Map<String, String> RENAME = Map.of("figure", "div", "figcaption", "div", "section", "div",
|
private static final Map<String, String> RENAME = Map.of("figure", "div", "figcaption", "div", "section", "div",
|
||||||
"article", "div", "aside", "div", "header", "div", "footer", "div", "abbr", "span", "q", "i", "del", "s");
|
"article", "div", "aside", "div", "header", "div", "footer", "div", "abbr", "span", "q", "i", "del", "s");
|
||||||
@@ -167,6 +167,13 @@ final class WikiHtml
|
|||||||
case "th":
|
case "th":
|
||||||
copy(attrs, open, "colspan", "rowspan", "align", "valign");
|
copy(attrs, open, "colspan", "rowspan", "align", "valign");
|
||||||
break;
|
break;
|
||||||
|
case "font":
|
||||||
|
String color = attrs.get("color");
|
||||||
|
if (color != null && color.matches("#[0-9A-Fa-f]{6}"))
|
||||||
|
{
|
||||||
|
open.append(" color=\"").append(color).append('"');
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "div":
|
case "div":
|
||||||
case "p":
|
case "p":
|
||||||
String style = attrs.getOrDefault("style", "").toLowerCase(Locale.ENGLISH);
|
String style = attrs.getOrDefault("style", "").toLowerCase(Locale.ENGLISH);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.essentialsplus.wiki;
|
package com.example.essentialsplus.wiki;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.player.PlayerHiscoreHistory;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
@@ -86,7 +87,7 @@ public class WikiOverlay extends Overlay
|
|||||||
|
|
||||||
Font small = FontManager.getRunescapeSmallFont();
|
Font small = FontManager.getRunescapeSmallFont();
|
||||||
g.setFont(small);
|
g.setFont(small);
|
||||||
window.externalWidth = g.getFontMetrics().stringWidth("Open in browser") + 14;
|
window.externalWidth = window.contentKey == null ? g.getFontMetrics().stringWidth("Open in browser") + 14 : 0;
|
||||||
if (border[0] != null && border[4] != null && border[5] != null)
|
if (border[0] != null && border[4] != null && border[5] != null)
|
||||||
{
|
{
|
||||||
window.border = Math.max(4, Math.min(24, Math.max(border[4].getWidth(), border[5].getHeight())));
|
window.border = Math.max(4, Math.min(24, Math.max(border[4].getWidth(), border[5].getHeight())));
|
||||||
@@ -136,7 +137,10 @@ public class WikiOverlay extends Overlay
|
|||||||
{
|
{
|
||||||
button(g, l.back, "Back", hover == WikiWindow.Part.BACK, pressed == WikiWindow.Part.BACK);
|
button(g, l.back, "Back", hover == WikiWindow.Part.BACK, pressed == WikiWindow.Part.BACK);
|
||||||
}
|
}
|
||||||
button(g, l.external, "Open in browser", hover == WikiWindow.Part.EXTERNAL, pressed == WikiWindow.Part.EXTERNAL);
|
if (window.contentKey == null)
|
||||||
|
{
|
||||||
|
button(g, l.external, "Open in browser", hover == WikiWindow.Part.EXTERNAL, pressed == WikiWindow.Part.EXTERNAL);
|
||||||
|
}
|
||||||
BufferedImage closeImg = hover == WikiWindow.Part.CLOSE && closeHover != null ? closeHover : close;
|
BufferedImage closeImg = hover == WikiWindow.Part.CLOSE && closeHover != null ? closeHover : close;
|
||||||
if (closeImg != null)
|
if (closeImg != null)
|
||||||
{
|
{
|
||||||
@@ -146,6 +150,25 @@ public class WikiOverlay extends Overlay
|
|||||||
{
|
{
|
||||||
button(g, l.close, "X", hover == WikiWindow.Part.CLOSE, pressed == WikiWindow.Part.CLOSE);
|
button(g, l.close, "X", hover == WikiWindow.Part.CLOSE, pressed == WikiWindow.Part.CLOSE);
|
||||||
}
|
}
|
||||||
|
if (l.playerSearch != null)
|
||||||
|
{
|
||||||
|
String label = window.playerSearchName.isEmpty() ? "Search any player" : "Search player: " + window.playerSearchName;
|
||||||
|
label = ellipsize(g.getFontMetrics(), label, l.playerSearch.width - 12);
|
||||||
|
button(g, l.playerSearch, label, hover == WikiWindow.Part.PLAYER_SEARCH, pressed == WikiWindow.Part.PLAYER_SEARCH);
|
||||||
|
}
|
||||||
|
if (l.playerRanges != null)
|
||||||
|
{
|
||||||
|
PlayerHiscoreHistory.Range[] ranges = PlayerHiscoreHistory.Range.values();
|
||||||
|
for (int i = 0; i < ranges.length; i++)
|
||||||
|
{
|
||||||
|
int x = l.playerRanges.x + l.playerRanges.width * i / ranges.length;
|
||||||
|
int right = l.playerRanges.x + l.playerRanges.width * (i + 1) / ranges.length;
|
||||||
|
Rectangle segment = new Rectangle(x, l.playerRanges.y, right - x, l.playerRanges.height);
|
||||||
|
boolean over = hover == WikiWindow.Part.PLAYER_RANGES && segment.contains(window.mouse);
|
||||||
|
boolean selected = window.playerStatsRange == ranges[i];
|
||||||
|
button(g, segment, ranges[i].label, over, selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// content
|
// content
|
||||||
g.setColor(new Color(0, 0, 0, 40));
|
g.setColor(new Color(0, 0, 0, 40));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.essentialsplus.wiki;
|
package com.example.essentialsplus.wiki;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.player.PlayerHiscoreHistory;
|
||||||
import java.awt.FontMetrics;
|
import java.awt.FontMetrics;
|
||||||
import java.awt.Point;
|
import java.awt.Point;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
@@ -46,12 +47,17 @@ public class WikiWindow
|
|||||||
static final int CHIP_LABEL = -1;
|
static final int CHIP_LABEL = -1;
|
||||||
static final int CHIP_MORE = -2;
|
static final int CHIP_MORE = -2;
|
||||||
static final int CHIP_LESS = -3;
|
static final int CHIP_LESS = -3;
|
||||||
|
private static final String PLAYER_EQUIPMENT_PREFIX = "player-equipment:";
|
||||||
|
|
||||||
/** Callbacks into the window manager. */
|
/** Callbacks into the window manager. */
|
||||||
interface Host
|
interface Host
|
||||||
{
|
{
|
||||||
void openInNewWindow(String title, String anchor, WikiWindow from);
|
void openInNewWindow(String title, String anchor, WikiWindow from);
|
||||||
|
|
||||||
|
void searchPlayer(WikiWindow window, String currentName);
|
||||||
|
|
||||||
|
void selectPlayerRange(WikiWindow window, PlayerHiscoreHistory.Range range);
|
||||||
|
|
||||||
void closed(WikiWindow window);
|
void closed(WikiWindow window);
|
||||||
|
|
||||||
void saveBounds(Rectangle bounds);
|
void saveBounds(Rectangle bounds);
|
||||||
@@ -61,7 +67,7 @@ public class WikiWindow
|
|||||||
|
|
||||||
enum Part
|
enum Part
|
||||||
{
|
{
|
||||||
NONE, TITLE, CLOSE, EXTERNAL, BACK, TOC, UP, DOWN, TRACK, THUMB, LEFT, RIGHT, HTRACK, HTHUMB, GRIP, CONTENT, FRAME
|
NONE, TITLE, CLOSE, EXTERNAL, BACK, PLAYER_SEARCH, PLAYER_RANGES, TOC, UP, DOWN, TRACK, THUMB, LEFT, RIGHT, HTRACK, HTHUMB, GRIP, CONTENT, FRAME
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Positions of all window parts for one set of bounds. */
|
/** Positions of all window parts for one set of bounds. */
|
||||||
@@ -72,13 +78,15 @@ public class WikiWindow
|
|||||||
final Rectangle close;
|
final Rectangle close;
|
||||||
final Rectangle external;
|
final Rectangle external;
|
||||||
final Rectangle back;
|
final Rectangle back;
|
||||||
|
final Rectangle playerSearch;
|
||||||
|
final Rectangle playerRanges;
|
||||||
final Rectangle toc;
|
final Rectangle toc;
|
||||||
final Rectangle content;
|
final Rectangle content;
|
||||||
final Rectangle vbar;
|
final Rectangle vbar;
|
||||||
final Rectangle hbar;
|
final Rectangle hbar;
|
||||||
final Rectangle grip;
|
final Rectangle grip;
|
||||||
|
|
||||||
Layout(Rectangle b, int border, int corner, int externalWidth, boolean horizontal, int tocHeight)
|
Layout(Rectangle b, int border, int corner, int externalWidth, boolean horizontal, int tocHeight, boolean showPlayerSearch)
|
||||||
{
|
{
|
||||||
bounds = b;
|
bounds = b;
|
||||||
int side = Math.max(border + 4, corner + 2);
|
int side = Math.max(border + 4, corner + 2);
|
||||||
@@ -89,6 +97,16 @@ public class WikiWindow
|
|||||||
int left = b.x + border + PAD;
|
int left = b.x + border + PAD;
|
||||||
int right = b.x + b.width - border - PAD;
|
int right = b.x + b.width - border - PAD;
|
||||||
int top = title.y + title.height + 2;
|
int top = title.y + title.height + 2;
|
||||||
|
playerSearch = showPlayerSearch ? new Rectangle(left, top, right - left, 24) : null;
|
||||||
|
if (playerSearch != null)
|
||||||
|
{
|
||||||
|
top = playerSearch.y + playerSearch.height + 4;
|
||||||
|
}
|
||||||
|
playerRanges = showPlayerSearch ? new Rectangle(left, top, right - left, 20) : null;
|
||||||
|
if (playerRanges != null)
|
||||||
|
{
|
||||||
|
top = playerRanges.y + playerRanges.height + 4;
|
||||||
|
}
|
||||||
toc = new Rectangle(left, top + 2, right - left, tocHeight);
|
toc = new Rectangle(left, top + 2, right - left, tocHeight);
|
||||||
if (tocHeight > 0)
|
if (tocHeight > 0)
|
||||||
{
|
{
|
||||||
@@ -194,6 +212,9 @@ public class WikiWindow
|
|||||||
volatile int corner = 6;
|
volatile int corner = 6;
|
||||||
volatile int externalWidth = 96;
|
volatile int externalWidth = 96;
|
||||||
volatile String title = "";
|
volatile String title = "";
|
||||||
|
volatile String contentKey;
|
||||||
|
volatile String playerSearchName = "";
|
||||||
|
volatile PlayerHiscoreHistory.Range playerStatsRange = PlayerHiscoreHistory.Range.LAST_VISIT;
|
||||||
volatile String status;
|
volatile String status;
|
||||||
volatile BufferedImage frame;
|
volatile BufferedImage frame;
|
||||||
volatile int scrollX;
|
volatile int scrollX;
|
||||||
@@ -259,12 +280,28 @@ public class WikiWindow
|
|||||||
|
|
||||||
Layout layout(Rectangle b)
|
Layout layout(Rectangle b)
|
||||||
{
|
{
|
||||||
return new Layout(b, border, corner, externalWidth, contentWidth > 0 && viewportWidth(b) < contentWidth, toc.height);
|
return new Layout(b, border, corner, externalWidth, contentWidth > 0 && viewportWidth(b) < contentWidth, toc.height,
|
||||||
|
isPlayerEquipmentContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private int viewportWidth(Rectangle b)
|
private int viewportWidth(Rectangle b)
|
||||||
{
|
{
|
||||||
return new Layout(b, border, corner, externalWidth, false, 0).content.width;
|
return new Layout(b, border, corner, externalWidth, false, 0, isPlayerEquipmentContent()).content.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPlayerEquipmentContent()
|
||||||
|
{
|
||||||
|
return contentKey != null && contentKey.startsWith(PLAYER_EQUIPMENT_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String playerSearchName()
|
||||||
|
{
|
||||||
|
return playerSearchName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerHiscoreHistory.Range playerStatsRange()
|
||||||
|
{
|
||||||
|
return playerStatsRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- pages
|
// ---------------------------------------------------------------- pages
|
||||||
@@ -290,6 +327,9 @@ public class WikiWindow
|
|||||||
}
|
}
|
||||||
canGoBack = !history.isEmpty();
|
canGoBack = !history.isEmpty();
|
||||||
int seq = ++loadSeq;
|
int seq = ++loadSeq;
|
||||||
|
contentKey = null;
|
||||||
|
playerSearchName = "";
|
||||||
|
playerStatsRange = PlayerHiscoreHistory.Range.LAST_VISIT;
|
||||||
currentTitle = pageTitle;
|
currentTitle = pageTitle;
|
||||||
title = pageTitle;
|
title = pageTitle;
|
||||||
status = "Loading...";
|
status = "Loading...";
|
||||||
@@ -360,6 +400,72 @@ public class WikiWindow
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens local, generated content in the same in-game window chrome as wiki pages (Swing thread). */
|
||||||
|
void openContent(String key, String pageTitle, String html)
|
||||||
|
{
|
||||||
|
openContent(key, pageTitle, html, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens local, generated content with cached images in the same wiki window. */
|
||||||
|
void openContent(String key, String pageTitle, String html, Map<String, BufferedImage> images)
|
||||||
|
{
|
||||||
|
if (!open)
|
||||||
|
{
|
||||||
|
open = true;
|
||||||
|
clampBounds();
|
||||||
|
}
|
||||||
|
loadSeq++;
|
||||||
|
boolean sameContent = key.equals(contentKey);
|
||||||
|
contentKey = key;
|
||||||
|
if (!sameContent)
|
||||||
|
{
|
||||||
|
playerStatsRange = PlayerHiscoreHistory.Range.LAST_VISIT;
|
||||||
|
}
|
||||||
|
playerSearchName = isPlayerEquipmentContent() && pageTitle.startsWith("Equipment: ")
|
||||||
|
? pageTitle.substring("Equipment: ".length()) : "";
|
||||||
|
currentTitle = null;
|
||||||
|
title = pageTitle;
|
||||||
|
history.clear();
|
||||||
|
canGoBack = false;
|
||||||
|
status = "Loading...";
|
||||||
|
frame = null;
|
||||||
|
view = null;
|
||||||
|
hoverLink = null;
|
||||||
|
sections = Collections.emptyList();
|
||||||
|
toc = Toc.EMPTY;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Map<String, BufferedImage> cachedImages = images == null ? Collections.emptyMap() : images;
|
||||||
|
WikiHtml.Result safe = WikiHtml.sanitize(html, Math.max(120, viewportWidth() - 24), cachedImages);
|
||||||
|
HTMLDocument doc = WikiDocumentView.buildDocument(safe.html, WikiApi.pageUrl(pageTitle), cachedImages);
|
||||||
|
WikiDocumentView v = new WikiDocumentView();
|
||||||
|
v.setDocument(doc, viewportWidth());
|
||||||
|
view = v;
|
||||||
|
status = null;
|
||||||
|
contentWidth = v.contentWidth();
|
||||||
|
contentHeight = v.height();
|
||||||
|
tocExpanded = false;
|
||||||
|
sections = v.sections();
|
||||||
|
updateToc();
|
||||||
|
scrollX = 0;
|
||||||
|
scrollY = 0;
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.debug("local wiki window content failed", e);
|
||||||
|
status = "Could not render content";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshContent()
|
||||||
|
{
|
||||||
|
if (contentKey != null && view != null)
|
||||||
|
{
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void back()
|
private void back()
|
||||||
{
|
{
|
||||||
Entry e = history.poll();
|
Entry e = history.poll();
|
||||||
@@ -510,7 +616,7 @@ public class WikiWindow
|
|||||||
toc = Toc.EMPTY;
|
toc = Toc.EMPTY;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Layout base = new Layout(bounds, border, corner, externalWidth, false, 0);
|
Layout base = new Layout(bounds, border, corner, externalWidth, false, 0, isPlayerEquipmentContent());
|
||||||
int width = base.vbar.x + base.vbar.width - base.content.x;
|
int width = base.vbar.x + base.vbar.width - base.content.x;
|
||||||
FontMetrics fm = metrics();
|
FontMetrics fm = metrics();
|
||||||
int rowStep = CHIP_H + CHIP_GAP;
|
int rowStep = CHIP_H + CHIP_GAP;
|
||||||
@@ -797,6 +903,14 @@ public class WikiWindow
|
|||||||
{
|
{
|
||||||
return Part.CLOSE;
|
return Part.CLOSE;
|
||||||
}
|
}
|
||||||
|
if (l.playerSearch != null && l.playerSearch.contains(p))
|
||||||
|
{
|
||||||
|
return Part.PLAYER_SEARCH;
|
||||||
|
}
|
||||||
|
if (l.playerRanges != null && l.playerRanges.contains(p))
|
||||||
|
{
|
||||||
|
return Part.PLAYER_RANGES;
|
||||||
|
}
|
||||||
if (l.external.contains(p))
|
if (l.external.contains(p))
|
||||||
{
|
{
|
||||||
return Part.EXTERNAL;
|
return Part.EXTERNAL;
|
||||||
@@ -857,6 +971,15 @@ public class WikiWindow
|
|||||||
return v.linkAt(p.x - l.content.x + scrollX, p.y - l.content.y + scrollY);
|
return v.linkAt(p.x - l.content.x + scrollX, p.y - l.content.y + scrollY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PlayerHiscoreHistory.Range rangeAt(Point p)
|
||||||
|
{
|
||||||
|
Layout l = layout();
|
||||||
|
PlayerHiscoreHistory.Range[] ranges = PlayerHiscoreHistory.Range.values();
|
||||||
|
int index = Math.max(0, Math.min(ranges.length - 1,
|
||||||
|
(p.x - l.playerRanges.x) * ranges.length / Math.max(1, l.playerRanges.width)));
|
||||||
|
return ranges[index];
|
||||||
|
}
|
||||||
|
|
||||||
void mousePressed(MouseEvent e)
|
void mousePressed(MouseEvent e)
|
||||||
{
|
{
|
||||||
Point p = e.getPoint();
|
Point p = e.getPoint();
|
||||||
@@ -992,6 +1115,13 @@ public class WikiWindow
|
|||||||
case BACK:
|
case BACK:
|
||||||
back();
|
back();
|
||||||
break;
|
break;
|
||||||
|
case PLAYER_SEARCH:
|
||||||
|
host.searchPlayer(this, playerSearchName);
|
||||||
|
break;
|
||||||
|
case PLAYER_RANGES:
|
||||||
|
playerStatsRange = rangeAt(e.getPoint());
|
||||||
|
host.selectPlayerRange(this, playerStatsRange);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package com.example.essentialsplus.wiki;
|
package com.example.essentialsplus.wiki;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.player.PlayerHiscoreHistory;
|
||||||
import java.awt.Point;
|
import java.awt.Point;
|
||||||
import java.awt.Rectangle;
|
import java.awt.Rectangle;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.event.MouseWheelEvent;
|
import java.awt.event.MouseWheelEvent;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.IntSupplier;
|
import java.util.function.IntSupplier;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
@@ -27,6 +32,8 @@ public class WikiWindows implements MouseListener, MouseWheelListener, WikiWindo
|
|||||||
private final IntSupplier canvasHeight;
|
private final IntSupplier canvasHeight;
|
||||||
private final Consumer<Rectangle> saveBounds;
|
private final Consumer<Rectangle> saveBounds;
|
||||||
private final Consumer<String> onPageOpened;
|
private final Consumer<String> onPageOpened;
|
||||||
|
private final BiConsumer<WikiWindow, String> onPlayerSearch;
|
||||||
|
private final BiConsumer<WikiWindow, PlayerHiscoreHistory.Range> onPlayerRange;
|
||||||
private final List<WikiWindow> windows = new CopyOnWriteArrayList<>();
|
private final List<WikiWindow> windows = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private Rectangle lastBounds;
|
private Rectangle lastBounds;
|
||||||
@@ -34,7 +41,8 @@ public class WikiWindows implements MouseListener, MouseWheelListener, WikiWindo
|
|||||||
private WikiWindow captured;
|
private WikiWindow captured;
|
||||||
|
|
||||||
public WikiWindows(WikiApi api, ExecutorService executor, IntSupplier canvasWidth, IntSupplier canvasHeight,
|
public WikiWindows(WikiApi api, ExecutorService executor, IntSupplier canvasWidth, IntSupplier canvasHeight,
|
||||||
Rectangle savedBounds, Consumer<Rectangle> saveBounds, Consumer<String> onPageOpened)
|
Rectangle savedBounds, Consumer<Rectangle> saveBounds, Consumer<String> onPageOpened,
|
||||||
|
BiConsumer<WikiWindow, String> onPlayerSearch, BiConsumer<WikiWindow, PlayerHiscoreHistory.Range> onPlayerRange)
|
||||||
{
|
{
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
@@ -43,6 +51,8 @@ public class WikiWindows implements MouseListener, MouseWheelListener, WikiWindo
|
|||||||
this.lastBounds = savedBounds;
|
this.lastBounds = savedBounds;
|
||||||
this.saveBounds = saveBounds;
|
this.saveBounds = saveBounds;
|
||||||
this.onPageOpened = onPageOpened;
|
this.onPageOpened = onPageOpened;
|
||||||
|
this.onPlayerSearch = onPlayerSearch;
|
||||||
|
this.onPlayerRange = onPlayerRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Windows from back to front (read by the overlay). */
|
/** Windows from back to front (read by the overlay). */
|
||||||
@@ -75,6 +85,91 @@ public class WikiWindows implements MouseListener, MouseWheelListener, WikiWindo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens or refreshes generated local content in an in-game window (any thread). */
|
||||||
|
public void openContent(String key, String title, String html)
|
||||||
|
{
|
||||||
|
openContent(key, title, html, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens or refreshes generated local content with local images in an in-game window (any thread). */
|
||||||
|
public void openContent(String key, String title, String html, Map<String, BufferedImage> images)
|
||||||
|
{
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
WikiWindow target = null;
|
||||||
|
for (WikiWindow window : windows)
|
||||||
|
{
|
||||||
|
if (window.isOpen() && key.equals(window.contentKey))
|
||||||
|
{
|
||||||
|
target = window;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
WikiWindow front = windows.isEmpty() ? null : windows.get(windows.size() - 1);
|
||||||
|
target = front == null ? create(null) : create(front);
|
||||||
|
}
|
||||||
|
target.openContent(key, title, html, images);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Updates generated content only while its window remains open (any thread). */
|
||||||
|
public void updateContent(String key, String title, String html, Map<String, BufferedImage> images)
|
||||||
|
{
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
for (WikiWindow window : windows)
|
||||||
|
{
|
||||||
|
if (window.isOpen() && key.equals(window.contentKey))
|
||||||
|
{
|
||||||
|
window.openContent(key, title, html, images);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replaces content in a specific open window without creating another one (any thread). */
|
||||||
|
public void updateContent(WikiWindow target, String key, String title, String html, Map<String, BufferedImage> images)
|
||||||
|
{
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
if (target != null && target.isOpen() && windows.contains(target))
|
||||||
|
{
|
||||||
|
target.openContent(key, title, html, images);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerHiscoreHistory.Range playerStatsRange(String key)
|
||||||
|
{
|
||||||
|
for (WikiWindow window : windows)
|
||||||
|
{
|
||||||
|
if (window.isOpen() && key.equals(window.contentKey))
|
||||||
|
{
|
||||||
|
return window.playerStatsRange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PlayerHiscoreHistory.Range.LAST_VISIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Repaints generated content after one of its cached images finishes loading (any thread). */
|
||||||
|
public void refreshContent(String key)
|
||||||
|
{
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
for (WikiWindow window : windows)
|
||||||
|
{
|
||||||
|
if (window.isOpen() && key.equals(window.contentKey))
|
||||||
|
{
|
||||||
|
window.refreshContent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void closeAll()
|
public void closeAll()
|
||||||
{
|
{
|
||||||
SwingUtilities.invokeLater(() ->
|
SwingUtilities.invokeLater(() ->
|
||||||
@@ -117,6 +212,18 @@ public class WikiWindows implements MouseListener, MouseWheelListener, WikiWindo
|
|||||||
create(from).open(title, anchor);
|
create(from).open(title, anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void searchPlayer(WikiWindow window, String currentName)
|
||||||
|
{
|
||||||
|
onPlayerSearch.accept(window, currentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selectPlayerRange(WikiWindow window, PlayerHiscoreHistory.Range range)
|
||||||
|
{
|
||||||
|
onPlayerRange.accept(window, range);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void closed(WikiWindow window)
|
public void closed(WikiWindow window)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public interface LiveDashboardConfig extends Config
|
|||||||
@ConfigItem(
|
@ConfigItem(
|
||||||
keyName = "port",
|
keyName = "port",
|
||||||
name = "Server Port (0 = auto)",
|
name = "Server Port (0 = auto)",
|
||||||
description = "Port fuer den integrierten Webserver. Bei 0 wird automatisch ein freier Port gewaehlt.",
|
description = "Port for the built-in web server. When set to 0, an available port is chosen automatically.",
|
||||||
position = 1
|
position = 1
|
||||||
)
|
)
|
||||||
default int port()
|
default int port()
|
||||||
@@ -22,8 +22,8 @@ public interface LiveDashboardConfig extends Config
|
|||||||
|
|
||||||
@ConfigItem(
|
@ConfigItem(
|
||||||
keyName = "autoOpenBrowser",
|
keyName = "autoOpenBrowser",
|
||||||
name = "Browser beim Start oeffnen",
|
name = "Open browser on startup",
|
||||||
description = "Oeffnet automatisch das Dashboard im Standardbrowser.",
|
description = "Automatically open the dashboard in your default browser.",
|
||||||
position = 2
|
position = 2
|
||||||
)
|
)
|
||||||
default boolean autoOpenBrowser()
|
default boolean autoOpenBrowser()
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import net.runelite.client.util.ImageUtil;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@PluginDescriptor(
|
@PluginDescriptor(
|
||||||
name = "Live Web Dashboard",
|
name = "Live Web Dashboard",
|
||||||
description = "Zeigt Echtzeit-Koordinaten, Vitals, Map, Hindernis-GPS-Navigation, Ausruestung, Inventar und Bank an",
|
description = "Shows real-time coordinates, stats, map, GPS navigation around obstacles, equipment, inventory, and bank contents",
|
||||||
tags = {"telemetry", "web", "dashboard", "live", "coordinates", "coords", "map", "bank", "inventory", "equipment", "ge", "gps"}
|
tags = {"telemetry", "web", "dashboard", "live", "coordinates", "coords", "map", "bank", "inventory", "equipment", "ge", "gps"}
|
||||||
)
|
)
|
||||||
public class LiveDashboardPlugin extends Plugin
|
public class LiveDashboardPlugin extends Plugin
|
||||||
@@ -434,7 +434,7 @@ public class LiveDashboardPlugin extends Plugin
|
|||||||
itemMap.put("name", "Item #" + item.getId());
|
itemMap.put("name", "Item #" + item.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
int gePrice = itemManager.getItemPrice(item.getId());
|
long gePrice = itemManager.getItemPrice(item.getId());
|
||||||
itemMap.put("price", gePrice);
|
itemMap.put("price", gePrice);
|
||||||
|
|
||||||
list.add(itemMap);
|
list.add(itemMap);
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
package com.example.essentialsplus;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.data.AccountData;
|
||||||
|
import com.example.essentialsplus.data.GeSlot;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.IntUnaryOperator;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.GrandExchangeOffer;
|
||||||
|
import net.runelite.api.GrandExchangeOfferState;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.widgets.ComponentID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class GrandExchangeTrackerTest
|
||||||
|
{
|
||||||
|
private static final int RUNITE_ORE = 451;
|
||||||
|
private static final int RUNITE_ORE_NOTED = 452;
|
||||||
|
|
||||||
|
private final IntUnaryOperator canonicalize = id -> id == RUNITE_ORE_NOTED ? RUNITE_ORE : id;
|
||||||
|
|
||||||
|
private Client client;
|
||||||
|
private GrandExchangeTracker tracker;
|
||||||
|
private AccountData acc;
|
||||||
|
private GrandExchangeOffer[] currentOffers;
|
||||||
|
private boolean geOpen;
|
||||||
|
private boolean bankOpen;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp()
|
||||||
|
{
|
||||||
|
tracker = new GrandExchangeTracker();
|
||||||
|
acc = new AccountData();
|
||||||
|
acc.containers = new HashMap<>();
|
||||||
|
acc.containers.put("inventory", new HashMap<>());
|
||||||
|
acc.containers.put("bank", new HashMap<>());
|
||||||
|
currentOffers = new GrandExchangeOffer[8];
|
||||||
|
geOpen = true;
|
||||||
|
bankOpen = true;
|
||||||
|
|
||||||
|
client = (Client) Proxy.newProxyInstance(
|
||||||
|
Client.class.getClassLoader(),
|
||||||
|
new Class<?>[]{Client.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
String name = method.getName();
|
||||||
|
if (name.equals("getGrandExchangeOffers"))
|
||||||
|
{
|
||||||
|
return currentOffers;
|
||||||
|
}
|
||||||
|
if (name.equals("getWidget") && args != null)
|
||||||
|
{
|
||||||
|
if (args.length == 2 && args[0].equals(InterfaceID.GE_OFFERS) && args[1].equals(0))
|
||||||
|
{
|
||||||
|
return geOpen ? mockWidget() : null;
|
||||||
|
}
|
||||||
|
if (args.length == 1 && args[0].equals(ComponentID.BANK_CONTAINER))
|
||||||
|
{
|
||||||
|
return bankOpen ? mockWidget() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Widget mockWidget()
|
||||||
|
{
|
||||||
|
return (Widget) Proxy.newProxyInstance(
|
||||||
|
Widget.class.getClassLoader(),
|
||||||
|
new Class<?>[]{Widget.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if (method.getName().equals("isHidden"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GrandExchangeOffer mockOffer(int itemId, int total, int done, long price, long spent, GrandExchangeOfferState state)
|
||||||
|
{
|
||||||
|
return (GrandExchangeOffer) Proxy.newProxyInstance(
|
||||||
|
GrandExchangeOffer.class.getClassLoader(),
|
||||||
|
new Class<?>[]{GrandExchangeOffer.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
switch (method.getName())
|
||||||
|
{
|
||||||
|
case "getItemId": return itemId;
|
||||||
|
case "getTotalQuantity": return total;
|
||||||
|
case "getQuantitySold": return done;
|
||||||
|
case "getPrice": return (Long) price;
|
||||||
|
case "getSpent": return (Long) spent;
|
||||||
|
case "getState": return state;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPartialCollectionToInventoryDoesNotDoubleCount()
|
||||||
|
{
|
||||||
|
// Offer: Buying 4000 Runite ore @ 10,000 gp
|
||||||
|
currentOffers[0] = mockOffer(RUNITE_ORE, 4000, 0, 10_000, 0, GrandExchangeOfferState.BUYING);
|
||||||
|
|
||||||
|
// Tick 1: establish baseline holdings
|
||||||
|
tracker.update(acc, client, canonicalize, true);
|
||||||
|
|
||||||
|
// Tick 2: slot created
|
||||||
|
Map<Integer, Long> ge = tracker.update(acc, client, canonicalize, true);
|
||||||
|
GeSlot slot = acc.geSlots[0];
|
||||||
|
Assert.assertNotNull(slot);
|
||||||
|
Assert.assertEquals(0L, slot.done);
|
||||||
|
Assert.assertEquals(40_000_000L, slot.escrowCoins);
|
||||||
|
Assert.assertEquals(0L, slot.collectItems);
|
||||||
|
|
||||||
|
// Now 1000 ores buy:
|
||||||
|
currentOffers[0] = mockOffer(RUNITE_ORE, 4000, 1000, 10_000, 10_000_000, GrandExchangeOfferState.BUYING);
|
||||||
|
ge = tracker.update(acc, client, canonicalize, true);
|
||||||
|
Assert.assertEquals(Long.valueOf(1000L), ge.get(RUNITE_ORE));
|
||||||
|
Assert.assertEquals(1000L, slot.collectItems);
|
||||||
|
Assert.assertEquals(0L, slot.collectedItems);
|
||||||
|
|
||||||
|
// User collects 600 ores to inventory:
|
||||||
|
acc.containers.get("inventory").put(RUNITE_ORE_NOTED, 600L);
|
||||||
|
ge = tracker.update(acc, client, canonicalize, true);
|
||||||
|
|
||||||
|
// GE should only hold the remaining 400 uncollected ores, NOT all 1000!
|
||||||
|
Assert.assertEquals(600L, slot.collectedItems);
|
||||||
|
Assert.assertEquals(400L, slot.collectItems);
|
||||||
|
Assert.assertEquals(Long.valueOf(400L), ge.get(RUNITE_ORE));
|
||||||
|
|
||||||
|
// Total Runite ores accounted across player + GE = 600 (inventory) + 400 (GE) = 1000!
|
||||||
|
long totalRunite = acc.containers.get("inventory").get(RUNITE_ORE_NOTED) + ge.get(RUNITE_ORE);
|
||||||
|
Assert.assertEquals(1000L, totalRunite);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPartialCollectionWithInterfaceClosedPreservesExactCount()
|
||||||
|
{
|
||||||
|
// Baseline
|
||||||
|
currentOffers[0] = mockOffer(RUNITE_ORE, 4000, 1000, 10_000, 10_000_000, GrandExchangeOfferState.BUYING);
|
||||||
|
|
||||||
|
tracker.update(acc, client, canonicalize, true);
|
||||||
|
tracker.update(acc, client, canonicalize, true);
|
||||||
|
|
||||||
|
// User closes GE window before compute runs
|
||||||
|
geOpen = false;
|
||||||
|
bankOpen = false;
|
||||||
|
|
||||||
|
// User collected 500 ores to inventory
|
||||||
|
acc.containers.get("inventory").put(RUNITE_ORE_NOTED, 500L);
|
||||||
|
Map<Integer, Long> ge = tracker.update(acc, client, canonicalize, true);
|
||||||
|
|
||||||
|
GeSlot slot = acc.geSlots[0];
|
||||||
|
// Collecting window and baseline floor both protect against double counting
|
||||||
|
Assert.assertEquals(500L, slot.collectedItems);
|
||||||
|
Assert.assertEquals(500L, slot.collectItems);
|
||||||
|
Assert.assertEquals(Long.valueOf(500L), ge.get(RUNITE_ORE));
|
||||||
|
|
||||||
|
long totalRunite = acc.containers.get("inventory").get(RUNITE_ORE_NOTED) + ge.get(RUNITE_ORE);
|
||||||
|
Assert.assertEquals(1000L, totalRunite);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnmigratedOldSlotAutoReconcilesWithoutDoubleCounting()
|
||||||
|
{
|
||||||
|
// Simulate slot loaded from old json:
|
||||||
|
// 3515 ores bought, s.collectedItems = 0, user already has 2000 in bank from earlier.
|
||||||
|
acc.geSlots = new GeSlot[8];
|
||||||
|
GeSlot old = new GeSlot();
|
||||||
|
old.item = RUNITE_ORE;
|
||||||
|
old.buy = true;
|
||||||
|
old.total = 4000;
|
||||||
|
old.price = 10_000;
|
||||||
|
old.done = 3515;
|
||||||
|
old.spent = 35_150_000;
|
||||||
|
old.collectedItems = 0;
|
||||||
|
old.collectItems = 3515;
|
||||||
|
old.escrowCoins = 4_850_000;
|
||||||
|
acc.geSlots[0] = old;
|
||||||
|
|
||||||
|
acc.containers.get("bank").put(RUNITE_ORE, 2000L);
|
||||||
|
|
||||||
|
currentOffers[0] = mockOffer(RUNITE_ORE, 4000, 3515, 10_000, 35_150_000, GrandExchangeOfferState.BUYING);
|
||||||
|
|
||||||
|
// Tick 1: establish previousHoldings
|
||||||
|
tracker.update(acc, client, canonicalize, true);
|
||||||
|
// Tick 2: processes offers and establishes baselineHoldings
|
||||||
|
tracker.update(acc, client, canonicalize, true);
|
||||||
|
|
||||||
|
GeSlot slot = acc.geSlots[0];
|
||||||
|
Assert.assertTrue(slot.baselineInitialized);
|
||||||
|
Assert.assertEquals(2000L, slot.baselineHoldings);
|
||||||
|
Assert.assertEquals(3515L, slot.collectItems);
|
||||||
|
|
||||||
|
// Now user collects 1000 ores into bank
|
||||||
|
acc.containers.get("bank").put(RUNITE_ORE, 3000L);
|
||||||
|
Map<Integer, Long> ge = tracker.update(acc, client, canonicalize, true);
|
||||||
|
|
||||||
|
Assert.assertEquals(1000L, slot.collectedItems);
|
||||||
|
Assert.assertEquals(2515L, slot.collectItems);
|
||||||
|
Assert.assertEquals(Long.valueOf(2515L), ge.get(RUNITE_ORE));
|
||||||
|
|
||||||
|
// Total Runite ores = 3000 (bank) + 2515 (GE) = 5515 (2000 baseline + 3515 from offer)
|
||||||
|
long totalRunite = acc.containers.get("bank").get(RUNITE_ORE) + ge.get(RUNITE_ORE);
|
||||||
|
Assert.assertEquals(5515L, totalRunite);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ import java.lang.reflect.Proxy;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import net.runelite.api.ChatMessageType;
|
import net.runelite.api.ChatMessageType;
|
||||||
import net.runelite.api.Client;
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.MenuEntry;
|
||||||
import net.runelite.api.events.ChatMessage;
|
import net.runelite.api.events.ChatMessage;
|
||||||
|
import net.runelite.api.events.MenuOptionClicked;
|
||||||
import net.runelite.api.gameval.ItemID;
|
import net.runelite.api.gameval.ItemID;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -24,13 +26,42 @@ public class ItemChargesFullTest
|
|||||||
return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[]{iface}, handler);
|
return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[]{iface}, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final java.util.Map<Integer, Integer> equipped = new java.util.HashMap<>();
|
||||||
|
private int playerAnim = -1;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup()
|
public void setup()
|
||||||
{
|
{
|
||||||
|
equipped.clear();
|
||||||
|
playerAnim = -1;
|
||||||
|
net.runelite.api.Player mockPlayer = mockInterface(net.runelite.api.Player.class, (p2, m2, a2) -> {
|
||||||
|
if ("getAnimation".equals(m2.getName())) return playerAnim;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
client = mockInterface(Client.class, (proxy, method, args) -> {
|
client = mockInterface(Client.class, (proxy, method, args) -> {
|
||||||
if ("getAccountHash".equals(method.getName())) return 123456789L;
|
if ("getAccountHash".equals(method.getName())) return 123456789L;
|
||||||
|
if ("getTickCount".equals(method.getName())) return 100;
|
||||||
if ("getVarbitValue".equals(method.getName())) return 0;
|
if ("getVarbitValue".equals(method.getName())) return 0;
|
||||||
if ("getItemContainer".equals(method.getName())) return null;
|
if ("getLocalPlayer".equals(method.getName())) {
|
||||||
|
return mockPlayer;
|
||||||
|
}
|
||||||
|
if ("getItemContainer".equals(method.getName())) {
|
||||||
|
Object arg0 = args[0];
|
||||||
|
int id = (arg0 instanceof Integer) ? (Integer) arg0 : (arg0 instanceof net.runelite.api.InventoryID ? ((net.runelite.api.InventoryID) arg0).getId() : -1);
|
||||||
|
if (id == net.runelite.api.gameval.InventoryID.WORN) {
|
||||||
|
return mockInterface(net.runelite.api.ItemContainer.class, (p2, m2, a2) -> {
|
||||||
|
if ("getItem".equals(m2.getName())) {
|
||||||
|
int slotIdx = (Integer) a2[0];
|
||||||
|
Integer itemId = equipped.get(slotIdx);
|
||||||
|
if (itemId != null) {
|
||||||
|
return new net.runelite.api.Item(itemId, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if ("getItemDefinition".equals(method.getName())) {
|
if ("getItemDefinition".equals(method.getName())) {
|
||||||
int id = (Integer) args[0];
|
int id = (Integer) args[0];
|
||||||
return mockInterface(net.runelite.api.ItemComposition.class, (p2, m2, a2) -> {
|
return mockInterface(net.runelite.api.ItemComposition.class, (p2, m2, a2) -> {
|
||||||
@@ -260,5 +291,281 @@ public class ItemChargesFullTest
|
|||||||
List<ItemCharges.ChargeValue> rod = itemCharges.chargesFor(net.runelite.api.ItemID.RING_OF_DUELING8);
|
List<ItemCharges.ChargeValue> rod = itemCharges.chargesFor(net.runelite.api.ItemID.RING_OF_DUELING8);
|
||||||
Assert.assertEquals("8", rod.get(0).getText());
|
Assert.assertEquals("8", rod.get(0).getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clickMenu(int itemId, String option, String target, int param1)
|
||||||
|
{
|
||||||
|
MenuEntry entry = mockInterface(MenuEntry.class, (proxy, method, args) -> {
|
||||||
|
if ("getItemId".equals(method.getName())) return itemId;
|
||||||
|
if ("getOption".equals(method.getName())) return option;
|
||||||
|
if ("getTarget".equals(method.getName())) return target;
|
||||||
|
if ("getParam1".equals(method.getName())) return param1;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
itemCharges.onMenuOptionClicked(new MenuOptionClicked(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPendantOfAtesFlow()
|
||||||
|
{
|
||||||
|
// 1. Initial state
|
||||||
|
List<ItemCharges.ChargeValue> empty = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES_EMPTY);
|
||||||
|
Assert.assertEquals(1, empty.size());
|
||||||
|
Assert.assertEquals("0", empty.get(0).getText());
|
||||||
|
|
||||||
|
List<ItemCharges.ChargeValue> initial = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals(1, initial.size());
|
||||||
|
Assert.assertEquals("?", initial.get(0).getText());
|
||||||
|
|
||||||
|
// 2. Check message
|
||||||
|
sendChat("The pendant has 450 charges.");
|
||||||
|
List<ItemCharges.ChargeValue> afterCheck = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals("450", afterCheck.get(0).getText());
|
||||||
|
|
||||||
|
// 3. Add charges
|
||||||
|
sendChat("Your add 50 frozen tears to your pendant. It now has 500 charges.");
|
||||||
|
List<ItemCharges.ChargeValue> afterAdd = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals("500", afterAdd.get(0).getText());
|
||||||
|
|
||||||
|
// 4. Direct teleport menu action (e.g. worn right-click "The Darkfrost")
|
||||||
|
clickMenu(ItemID.PENDANT_OF_ATES, "The Darkfrost", "<col=ff9040>Pendant of Ates</col>", 0);
|
||||||
|
List<ItemCharges.ChargeValue> afterTele = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals("499", afterTele.get(0).getText());
|
||||||
|
|
||||||
|
// 5. Interface 879 teleport click
|
||||||
|
clickMenu(-1, "Twilight Temple", "", (879 << 16) | 1);
|
||||||
|
List<ItemCharges.ChargeValue> afterInterfaceTele = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals("498", afterInterfaceTele.get(0).getText());
|
||||||
|
|
||||||
|
// 6. Already at location (does not use charge)
|
||||||
|
sendChat("The pendant doesn't respond as you appear to already be at that location.");
|
||||||
|
List<ItemCharges.ChargeValue> afterAlreadyThere = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals("499", afterAlreadyThere.get(0).getText());
|
||||||
|
|
||||||
|
// 7. Fully charged message
|
||||||
|
sendChat("The pendant is already fully charged.");
|
||||||
|
List<ItemCharges.ChargeValue> afterFull = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals("1K", afterFull.get(0).getText());
|
||||||
|
|
||||||
|
// 8. Uncharge message
|
||||||
|
sendChat("You uncharge your pendant by removing 1,000 frozen tears from it.");
|
||||||
|
List<ItemCharges.ChargeValue> afterUncharge = itemCharges.chargesFor(ItemID.PENDANT_OF_ATES);
|
||||||
|
Assert.assertEquals("0", afterUncharge.get(0).getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTridentFlow()
|
||||||
|
{
|
||||||
|
// 1. Initial state
|
||||||
|
Assert.assertEquals("?", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TOXIC_TOTS_UNCHARGED).get(0).getText());
|
||||||
|
Assert.assertEquals("?", itemCharges.chargesFor(ItemID.TOTS_CHARGED).get(0).getText());
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TOTS_UNCHARGED).get(0).getText());
|
||||||
|
|
||||||
|
// 2. Check messages
|
||||||
|
sendChat("Your Trident of the swamp has 2,450 charges.");
|
||||||
|
Assert.assertEquals("2.5K", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Trident of the swamp (e) has 8,500 charges.");
|
||||||
|
Assert.assertEquals("8.5K", itemCharges.chargesFor(ItemID.TOXIC_TOTS_I_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Trident of the seas has 1,500 charges.");
|
||||||
|
Assert.assertEquals("1.5K", itemCharges.chargesFor(ItemID.TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Trident of the seas (e) has 6,000 charges.");
|
||||||
|
Assert.assertEquals("6K", itemCharges.chargesFor(ItemID.TOTS_I_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
// 3. Decrement
|
||||||
|
itemCharges.decrementCount("trident_of_the_swamp", 50);
|
||||||
|
Assert.assertEquals("2.4K", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
// 4. Low warning
|
||||||
|
sendChat("Your trident only has 100 charges left!");
|
||||||
|
Assert.assertEquals("100", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
// 5. Uncharge menu option
|
||||||
|
clickMenu(ItemID.TOXIC_TOTS_CHARGED, "Uncharge", "<col=ff9040>Trident of the swamp</col>", 0);
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
// 6. Zero / one / empty messages
|
||||||
|
sendChat("Your Trident of the swamp has one charge.");
|
||||||
|
Assert.assertEquals("1", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Trident of the swamp has no charges.");
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your trident has run out of charges.");
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSanguinestiAndTumekensAndWarped()
|
||||||
|
{
|
||||||
|
// Sanguinesti Staff
|
||||||
|
Assert.assertEquals("?", itemCharges.chargesFor(ItemID.SANGUINESTI_STAFF).get(0).getText());
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.SANGUINESTI_STAFF_UNCHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Sanguinesti staff has 1,500 charges remaining.");
|
||||||
|
Assert.assertEquals("1.5K", itemCharges.chargesFor(ItemID.SANGUINESTI_STAFF).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("You apply an additional 500 charges to your Sanguinesti staff. It now has 2,000 charges in total.");
|
||||||
|
Assert.assertEquals("2K", itemCharges.chargesFor(ItemID.SANGUINESTI_STAFF).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Sanguinesti staff is already fully charged.");
|
||||||
|
Assert.assertEquals("20K", itemCharges.chargesFor(ItemID.SANGUINESTI_STAFF).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Sanguinesti staff has run out of charges.");
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.SANGUINESTI_STAFF).get(0).getText());
|
||||||
|
|
||||||
|
// Tumeken's Shadow
|
||||||
|
Assert.assertEquals("?", itemCharges.chargesFor(ItemID.TUMEKENS_SHADOW).get(0).getText());
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TUMEKENS_SHADOW_UNCHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Tumeken's shadow has 450 charges remaining.");
|
||||||
|
Assert.assertEquals("450", itemCharges.chargesFor(ItemID.TUMEKENS_SHADOW).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("You apply an additional 150 charges to your Tumeken's shadow. It now has 600 charges in total.");
|
||||||
|
Assert.assertEquals("600", itemCharges.chargesFor(ItemID.TUMEKENS_SHADOW).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Tumeken's shadow has run out of charges.");
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TUMEKENS_SHADOW).get(0).getText());
|
||||||
|
|
||||||
|
// Warped Sceptre
|
||||||
|
Assert.assertEquals("?", itemCharges.chargesFor(ItemID.WARPED_SCEPTRE).get(0).getText());
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.WARPED_SCEPTRE_UNCHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your warped sceptre has 5,000 charges remaining.");
|
||||||
|
Assert.assertEquals("5K", itemCharges.chargesFor(ItemID.WARPED_SCEPTRE).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your warped sceptre has run out of charges!");
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.WARPED_SCEPTRE).get(0).getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSerpentineAndTomesAndBloodFury()
|
||||||
|
{
|
||||||
|
// Serpentine Helm
|
||||||
|
Assert.assertEquals("?", itemCharges.chargesFor(ItemID.SERPENTINE_HELM_CHARGED).get(0).getText());
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.SERPENTINE_HELM).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Scales: 11,000 (100.0%)");
|
||||||
|
Assert.assertEquals("11K", itemCharges.chargesFor(ItemID.SERPENTINE_HELM_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your serpentine helm has run out of Zulrah's scales.");
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.SERPENTINE_HELM_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
// Tomes
|
||||||
|
Assert.assertEquals("?", itemCharges.chargesFor(ItemID.TOME_OF_FIRE).get(0).getText());
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TOME_OF_FIRE_UNCHARGED).get(0).getText());
|
||||||
|
sendChat("Your tome has been charged with Burnt Pages. It currently holds 200 charges.");
|
||||||
|
Assert.assertEquals("200", itemCharges.chargesFor(ItemID.TOME_OF_FIRE).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your tome has been charged with Water Pages. It currently holds 500 charges.");
|
||||||
|
Assert.assertEquals("500", itemCharges.chargesFor(ItemID.TOME_OF_WATER).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your tome has been charged with Earth Pages. It currently holds 300 charges.");
|
||||||
|
Assert.assertEquals("300", itemCharges.chargesFor(ItemID.TOME_OF_EARTH).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Tome of Fire is now empty.");
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.TOME_OF_FIRE).get(0).getText());
|
||||||
|
|
||||||
|
// Blood Fury
|
||||||
|
sendChat("You have successfully created an Amulet of blood fury.");
|
||||||
|
Assert.assertEquals("10K", itemCharges.chargesFor(ItemID.BLOOD_AMULET).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Amulet of blood fury will work for 9,016 more hits.");
|
||||||
|
Assert.assertEquals("9.0K", itemCharges.chargesFor(ItemID.BLOOD_AMULET).get(0).getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWildernessAndSufferingAndUtilityWeapons()
|
||||||
|
{
|
||||||
|
// Ether weapons
|
||||||
|
sendChat("Your bow has 666 charges left powering it.");
|
||||||
|
Assert.assertEquals("666", itemCharges.chargesFor(ItemID.WILD_CAVE_BOW_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("You add a further 1,000 revenant ether to your weapon, giving it a total of 1,666 charges.");
|
||||||
|
Assert.assertEquals("1.7K", itemCharges.chargesFor(ItemID.WILD_CAVE_BOW_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
// Ring of Suffering
|
||||||
|
Assert.assertEquals("0", itemCharges.chargesFor(ItemID.ZENYTE_RING_ENCHANTED).get(0).getText());
|
||||||
|
sendChat("Your Ring of suffering (i) has 50,000 charges remaining.");
|
||||||
|
Assert.assertEquals("50K", itemCharges.chargesFor(ItemID.ZENYTE_RING_ENCHANTED_RECOIL).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("You load 25 rings of recoil into your Ring of suffering. It now has 51,000 charges.");
|
||||||
|
Assert.assertEquals("51K", itemCharges.chargesFor(ItemID.ZENYTE_RING_ENCHANTED_RECOIL).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your Ring of suffering is already fully charged.");
|
||||||
|
Assert.assertEquals("100K", itemCharges.chargesFor(ItemID.ZENYTE_RING_ENCHANTED_RECOIL).get(0).getText());
|
||||||
|
|
||||||
|
// Bonecrusher & Ash Sanctifier
|
||||||
|
sendChat("Your bonecrusher has 2,500 charges left.");
|
||||||
|
Assert.assertEquals("2.5K", itemCharges.chargesFor(ItemID.BONECRUSHER).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your ash sanctifier has 1,200 charges left.");
|
||||||
|
Assert.assertEquals("1.2K", itemCharges.chargesFor(ItemID.ASH_SANCTIFIER).get(0).getText());
|
||||||
|
|
||||||
|
// Arclight & Crystal Halberd & Crystal Bow
|
||||||
|
sendChat("Your arclight has 6,397 charges left.");
|
||||||
|
Assert.assertEquals("6.4K", itemCharges.chargesFor(ItemID.ARCLIGHT).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your crystal halberd has 278 charges remaining.");
|
||||||
|
Assert.assertEquals("278", itemCharges.chargesFor(ItemID.CRYSTAL_HALBERD).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("Your crystal bow has 299 charges remaining.");
|
||||||
|
Assert.assertEquals("299", itemCharges.chargesFor(ItemID.CRYSTAL_BOW).get(0).getText());
|
||||||
|
|
||||||
|
// Venator Bow
|
||||||
|
sendChat("Your Venator bow has 500 charges remaining.");
|
||||||
|
Assert.assertEquals("500", itemCharges.chargesFor(ItemID.VENATOR_BOW).get(0).getText());
|
||||||
|
|
||||||
|
sendChat("You use 100 ancient essence to charge your venator bow. It now has 600 charges.");
|
||||||
|
Assert.assertEquals("600", itemCharges.chargesFor(ItemID.VENATOR_BOW).get(0).getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerAnimation(int anim)
|
||||||
|
{
|
||||||
|
playerAnim = anim;
|
||||||
|
net.runelite.api.Player me = client.getLocalPlayer();
|
||||||
|
net.runelite.api.events.AnimationChanged event = new net.runelite.api.events.AnimationChanged();
|
||||||
|
event.setActor(me);
|
||||||
|
itemCharges.onAnimationChanged(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCombatAnimationDecrements()
|
||||||
|
{
|
||||||
|
// 1. Trident attack animation (11430)
|
||||||
|
sendChat("Your Trident of the swamp has 2,500 charges.");
|
||||||
|
Assert.assertEquals("2.5K", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
|
||||||
|
equipped.put(net.runelite.api.EquipmentInventorySlot.WEAPON.getSlotIdx(), ItemID.TOXIC_TOTS_CHARGED);
|
||||||
|
triggerAnimation(11430);
|
||||||
|
Assert.assertEquals("2.5K", itemCharges.chargesFor(ItemID.TOXIC_TOTS_CHARGED).get(0).getText());
|
||||||
|
Assert.assertEquals(2499, itemCharges.getTrackedCount("trident_of_the_swamp"));
|
||||||
|
|
||||||
|
// 2. Tumeken's shadow attack animation (9493)
|
||||||
|
sendChat("Tumeken's shadow has 500 charges remaining.");
|
||||||
|
equipped.put(net.runelite.api.EquipmentInventorySlot.WEAPON.getSlotIdx(), ItemID.TUMEKENS_SHADOW);
|
||||||
|
triggerAnimation(9493);
|
||||||
|
Assert.assertEquals(499, itemCharges.getTrackedCount("tumekens_shadow"));
|
||||||
|
|
||||||
|
// 3. Warped sceptre attack animation (10501)
|
||||||
|
sendChat("Your warped sceptre has 300 charges remaining.");
|
||||||
|
equipped.put(net.runelite.api.EquipmentInventorySlot.WEAPON.getSlotIdx(), ItemID.WARPED_SCEPTRE);
|
||||||
|
triggerAnimation(10501);
|
||||||
|
Assert.assertEquals(299, itemCharges.getTrackedCount("warped_sceptre"));
|
||||||
|
|
||||||
|
// 4. Tome of fire in shield slot (anim 711)
|
||||||
|
sendChat("Your tome has been charged with Burnt Pages. It currently holds 100 charges.");
|
||||||
|
equipped.put(net.runelite.api.EquipmentInventorySlot.SHIELD.getSlotIdx(), ItemID.TOME_OF_FIRE);
|
||||||
|
triggerAnimation(711);
|
||||||
|
Assert.assertEquals(99, itemCharges.getTrackedCount("tome_of_fire"));
|
||||||
|
|
||||||
|
// 5. Blood fury in amulet slot (anim 1658 whip attack)
|
||||||
|
sendChat("Your Amulet of blood fury will work for 1,000 more hits.");
|
||||||
|
equipped.put(net.runelite.api.EquipmentInventorySlot.AMULET.getSlotIdx(), ItemID.BLOOD_AMULET);
|
||||||
|
triggerAnimation(1658);
|
||||||
|
Assert.assertEquals(999, itemCharges.getTrackedCount("amulet_of_blood_fury"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
package com.example.essentialsplus;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.data.AccountData;
|
||||||
|
import com.example.essentialsplus.WealthStore;
|
||||||
|
import com.example.essentialsplus.helpers.ItemCharges;
|
||||||
|
import com.example.essentialsplus.helpers.ItemInformationOverlay;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.Item;
|
||||||
|
import net.runelite.api.ItemContainer;
|
||||||
|
import net.runelite.api.MenuEntry;
|
||||||
|
import net.runelite.api.events.ItemContainerChanged;
|
||||||
|
import net.runelite.api.gameval.InventoryID;
|
||||||
|
import net.runelite.api.gameval.ItemID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
|
import net.runelite.client.ui.overlay.tooltip.Tooltip;
|
||||||
|
import net.runelite.client.ui.overlay.tooltip.TooltipManager;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class ItemInformationTest
|
||||||
|
{
|
||||||
|
private static final int SHARK_ID = 383;
|
||||||
|
private static final int SHARK_NOTED_ID = 384;
|
||||||
|
private static final int LOBSTER_ID = 379;
|
||||||
|
|
||||||
|
private Client client;
|
||||||
|
private EssentialsPlusConfig config;
|
||||||
|
private EssentialsPlusPlugin plugin;
|
||||||
|
private TooltipManager tooltipManager;
|
||||||
|
private ItemInformationOverlay overlay;
|
||||||
|
private ItemCharges itemCharges;
|
||||||
|
|
||||||
|
private final Map<String, Object> configStore = new HashMap<>();
|
||||||
|
private ItemContainer mockBankContainer;
|
||||||
|
private MenuEntry[] currentMenuEntries;
|
||||||
|
private AccountData testAccount;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> T mockInterface(Class<T> iface, java.lang.reflect.InvocationHandler handler)
|
||||||
|
{
|
||||||
|
return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[]{iface}, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() throws Exception
|
||||||
|
{
|
||||||
|
configStore.clear();
|
||||||
|
mockBankContainer = null;
|
||||||
|
currentMenuEntries = new MenuEntry[0];
|
||||||
|
|
||||||
|
client = mockInterface(Client.class, (proxy, method, args) -> {
|
||||||
|
String name = method.getName();
|
||||||
|
if ("getAccountHash".equals(name)) return 123456789L;
|
||||||
|
if ("getVarbitValue".equals(name)) return 0;
|
||||||
|
if ("isMenuOpen".equals(name)) return false;
|
||||||
|
if ("getItemContainer".equals(name)) {
|
||||||
|
Object arg = args[0];
|
||||||
|
int id = arg instanceof Number ? ((Number) arg).intValue() : (arg != null ? ((net.runelite.api.InventoryID) arg).getId() : -1);
|
||||||
|
if (id == InventoryID.BANK) {
|
||||||
|
return mockBankContainer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ("getMenuEntries".equals(name)) return currentMenuEntries;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
config = mockInterface(EssentialsPlusConfig.class, (proxy, method, args) -> {
|
||||||
|
String name = method.getName();
|
||||||
|
Object val = configStore.get(name);
|
||||||
|
if (val != null) return val;
|
||||||
|
if ("itemInformation".equals(name)) return false;
|
||||||
|
if ("itemInformationGe".equals(name)) return true;
|
||||||
|
if ("itemInformationHa".equals(name)) return true;
|
||||||
|
if ("itemInformationBanked".equals(name)) return true;
|
||||||
|
if ("itemInformationCharges".equals(name)) return true;
|
||||||
|
if ("itemInformationChargesWeapons".equals(name)) return true;
|
||||||
|
if ("itemInformationChargesBags".equals(name)) return true;
|
||||||
|
if ("itemInformationBank".equals(name)) return true;
|
||||||
|
if ("itemInformationEquipment".equals(name)) return true;
|
||||||
|
if ("itemInformationChargesBank".equals(name)) return true;
|
||||||
|
if ("itemPrices".equals(name)) return false;
|
||||||
|
if ("itemCharges".equals(name)) return false;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
tooltipManager = new TooltipManager();
|
||||||
|
|
||||||
|
plugin = new EssentialsPlusPlugin()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public boolean isItemInformationEnabled()
|
||||||
|
{
|
||||||
|
Boolean val = (Boolean) configStore.get("itemInformation");
|
||||||
|
if (val != null)
|
||||||
|
{
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
Boolean legacyPrices = (Boolean) configStore.get("itemPrices");
|
||||||
|
Boolean legacyCharges = (Boolean) configStore.get("itemCharges");
|
||||||
|
if (Boolean.TRUE.equals(legacyPrices) || Boolean.TRUE.equals(legacyCharges))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getHelperValue(String key, Class<T> type, T def)
|
||||||
|
{
|
||||||
|
Object val = configStore.get(key);
|
||||||
|
if (val != null)
|
||||||
|
{
|
||||||
|
return (T) val;
|
||||||
|
}
|
||||||
|
Map<String, String> fallbacks = Map.ofEntries(
|
||||||
|
Map.entry("itemInformationGe", "itemPricesGe"),
|
||||||
|
Map.entry("itemInformationHa", "itemPricesHa"),
|
||||||
|
Map.entry("itemInformationCharges", "itemCharges"),
|
||||||
|
Map.entry("itemInformationChargesWeapons", "itemChargesWeapons"),
|
||||||
|
Map.entry("itemInformationChargesBags", "itemChargesBags"),
|
||||||
|
Map.entry("itemInformationChargesBank", "itemChargesBank"),
|
||||||
|
Map.entry("itemInformationBank", "itemChargesBank"),
|
||||||
|
Map.entry("itemInformationSize", "itemChargesSize"),
|
||||||
|
Map.entry("itemInformationColor", "itemChargesColor"),
|
||||||
|
Map.entry("itemInformationSecondaryColor", "itemChargesSecondaryColor"),
|
||||||
|
Map.entry("itemInformationLowColor", "itemChargesLowColor"),
|
||||||
|
Map.entry("itemInformationLowAt", "itemChargesLowAt")
|
||||||
|
);
|
||||||
|
String fb = fallbacks.get(key);
|
||||||
|
if (fb != null)
|
||||||
|
{
|
||||||
|
Object fbVal = configStore.get(fb);
|
||||||
|
if (fbVal != null)
|
||||||
|
{
|
||||||
|
return (T) fbVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHelperOption(String key, Object value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
configStore.remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
configStore.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int canonicalize(int id)
|
||||||
|
{
|
||||||
|
if (id == SHARK_NOTED_ID)
|
||||||
|
{
|
||||||
|
return SHARK_ID;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setField(plugin, "client", client);
|
||||||
|
setField(plugin, "config", config);
|
||||||
|
|
||||||
|
WealthStore store = new WealthStore(new com.google.gson.Gson());
|
||||||
|
setField(plugin, "store", store);
|
||||||
|
setField(plugin, "activeKey", "acc_123");
|
||||||
|
|
||||||
|
testAccount = new AccountData();
|
||||||
|
testAccount.key = "acc_123";
|
||||||
|
testAccount.name = "TestPlayer";
|
||||||
|
store.put(testAccount);
|
||||||
|
|
||||||
|
itemCharges = new ItemCharges(client, config, plugin, null, null);
|
||||||
|
setField(plugin, "itemCharges", itemCharges);
|
||||||
|
|
||||||
|
overlay = new ItemInformationOverlay(client, config, plugin, null, tooltipManager)
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public int canonicalize(int id)
|
||||||
|
{
|
||||||
|
if (id == SHARK_NOTED_ID)
|
||||||
|
{
|
||||||
|
return SHARK_ID;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getGePrice(int id)
|
||||||
|
{
|
||||||
|
if (id == SHARK_ID) return 1000;
|
||||||
|
if (id == LOBSTER_ID) return 200;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getHaPrice(int id)
|
||||||
|
{
|
||||||
|
if (id == SHARK_ID) return 600;
|
||||||
|
if (id == LOBSTER_ID) return 120;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String fieldName, Object val) throws Exception
|
||||||
|
{
|
||||||
|
Class<?> clazz = target.getClass();
|
||||||
|
while (clazz != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Field f = clazz.getDeclaredField(fieldName);
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(target, val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (NoSuchFieldException e)
|
||||||
|
{
|
||||||
|
clazz = clazz.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchFieldException(fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tooltip lastTooltip()
|
||||||
|
{
|
||||||
|
List<Tooltip> list = tooltipManager.getTooltips();
|
||||||
|
return (list == null || list.isEmpty()) ? null : list.get(list.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. GE-Preis allein
|
||||||
|
@Test
|
||||||
|
public void test1_GePriceAlone()
|
||||||
|
{
|
||||||
|
String text = overlay.buildTooltipText(SHARK_ID, 2, true, false, false);
|
||||||
|
Assert.assertTrue(text.contains("GE:"));
|
||||||
|
Assert.assertTrue(text.contains("2,000 gp") || text.contains("2.0k gp") || text.contains("2k gp"));
|
||||||
|
Assert.assertFalse(text.contains("HA:"));
|
||||||
|
Assert.assertFalse(text.contains("Banked:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. HA-Wert allein
|
||||||
|
@Test
|
||||||
|
public void test2_HaPriceAlone()
|
||||||
|
{
|
||||||
|
String text = overlay.buildTooltipText(SHARK_ID, 2, false, true, false);
|
||||||
|
Assert.assertFalse(text.contains("GE:"));
|
||||||
|
Assert.assertTrue(text.contains("HA:"));
|
||||||
|
Assert.assertTrue(text.contains("1,200 gp") || text.contains("1.2k gp") || text.contains("1k gp"));
|
||||||
|
Assert.assertFalse(text.contains("Banked:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. GE und HA zusammen
|
||||||
|
@Test
|
||||||
|
public void test3_GeAndHaTogether()
|
||||||
|
{
|
||||||
|
String text = overlay.buildTooltipText(SHARK_ID, 1, true, true, false);
|
||||||
|
Assert.assertTrue(text.contains("GE:"));
|
||||||
|
Assert.assertTrue(text.contains("HA:"));
|
||||||
|
Assert.assertFalse(text.contains("</br>"));
|
||||||
|
Assert.assertFalse(text.contains("Banked:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Bankmenge bei normalem Item
|
||||||
|
@Test
|
||||||
|
public void test4_BankedQuantityNormalItem()
|
||||||
|
{
|
||||||
|
testAccount.bankKnown = true;
|
||||||
|
testAccount.containers.put("bank", new HashMap<>(Map.of(SHARK_ID, 300L)));
|
||||||
|
|
||||||
|
String text = overlay.buildTooltipText(SHARK_ID, 1, true, true, true);
|
||||||
|
Assert.assertTrue(text.contains("GE:"));
|
||||||
|
Assert.assertTrue(text.contains("HA:"));
|
||||||
|
Assert.assertTrue(text.contains("</br>Banked:"));
|
||||||
|
Assert.assertTrue(text.contains("300"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Bankmenge bei noted/unnoted Kombination
|
||||||
|
@Test
|
||||||
|
public void test5_BankedQuantityNotedUnnotedCombination()
|
||||||
|
{
|
||||||
|
testAccount.bankKnown = true;
|
||||||
|
testAccount.containers.put("bank", new HashMap<>(Map.of(SHARK_ID, 500L)));
|
||||||
|
|
||||||
|
long qtyForNoted = plugin.getBankedQuantity(SHARK_NOTED_ID);
|
||||||
|
Assert.assertEquals(500L, qtyForNoted);
|
||||||
|
|
||||||
|
String text = overlay.buildTooltipText(overlay.canonicalize(SHARK_NOTED_ID), 1, false, false, true);
|
||||||
|
Assert.assertTrue(text.contains("Banked:"));
|
||||||
|
Assert.assertTrue(text.contains("500"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. unbekannter Bankbestand wird nicht angezeigt
|
||||||
|
@Test
|
||||||
|
public void test6_UnknownBankQuantityNotDisplayed()
|
||||||
|
{
|
||||||
|
testAccount.bankKnown = false;
|
||||||
|
mockBankContainer = null;
|
||||||
|
|
||||||
|
long banked = plugin.getBankedQuantity(SHARK_ID);
|
||||||
|
Assert.assertEquals(-1L, banked);
|
||||||
|
|
||||||
|
String text = overlay.buildTooltipText(SHARK_ID, 1, true, true, true);
|
||||||
|
Assert.assertFalse(text.contains("Banked:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. bekannter Bankbestand von 0
|
||||||
|
@Test
|
||||||
|
public void test7_KnownBankQuantityZero()
|
||||||
|
{
|
||||||
|
testAccount.bankKnown = true;
|
||||||
|
testAccount.containers.put("bank", new HashMap<>());
|
||||||
|
|
||||||
|
long banked = plugin.getBankedQuantity(SHARK_ID);
|
||||||
|
Assert.assertEquals(0L, banked);
|
||||||
|
|
||||||
|
String text = overlay.buildTooltipText(SHARK_ID, 1, false, false, true);
|
||||||
|
Assert.assertTrue(text.contains("Banked:"));
|
||||||
|
Assert.assertTrue(text.contains("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Bankbestand aktualisiert sich nach Containeränderung
|
||||||
|
@Test
|
||||||
|
public void test8_BankQuantityUpdatesOnContainerChange()
|
||||||
|
{
|
||||||
|
testAccount.bankKnown = true;
|
||||||
|
testAccount.containers.put("bank", new HashMap<>(Map.of(SHARK_ID, 100L)));
|
||||||
|
Assert.assertEquals(100L, plugin.getBankedQuantity(SHARK_ID));
|
||||||
|
|
||||||
|
Item sharkItem = new Item(SHARK_ID, 250);
|
||||||
|
ItemContainer newBank = mockInterface(ItemContainer.class, (p, m, a) -> {
|
||||||
|
if ("getItems".equals(m.getName())) {
|
||||||
|
return new Item[]{sharkItem};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ItemContainerChanged event = new ItemContainerChanged(InventoryID.BANK, newBank);
|
||||||
|
plugin.onItemContainerChanged(event);
|
||||||
|
|
||||||
|
Assert.assertEquals(250L, plugin.getBankedQuantity(SHARK_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. gemeinsamer Hauptschalter deaktiviert beide Overlay-Arten
|
||||||
|
@Test
|
||||||
|
public void test9_MasterSwitchDisablesBothOverlays()
|
||||||
|
{
|
||||||
|
configStore.put("itemInformation", false);
|
||||||
|
configStore.put("itemPrices", false);
|
||||||
|
configStore.put("itemCharges", false);
|
||||||
|
|
||||||
|
Assert.assertFalse(plugin.isItemInformationEnabled());
|
||||||
|
Assert.assertFalse(itemCharges.isEnabled());
|
||||||
|
|
||||||
|
Widget slot = mockInterface(Widget.class, (p, m, a) -> {
|
||||||
|
if ("getId".equals(m.getName())) return 149 << 16;
|
||||||
|
if ("getItemId".equals(m.getName())) return SHARK_ID;
|
||||||
|
if ("getItemQuantity".equals(m.getName())) return 1;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
MenuEntry entry = mockInterface(MenuEntry.class, (p, m, a) -> {
|
||||||
|
if ("getWidget".equals(m.getName())) return slot;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
currentMenuEntries = new MenuEntry[]{entry};
|
||||||
|
|
||||||
|
overlay.render(null);
|
||||||
|
Assert.assertNull(lastTooltip());
|
||||||
|
|
||||||
|
List<ItemCharges.ChargeValue> charges = itemCharges.chargesFor(ItemID.TOXIC_BLOWPIPE_LOADED);
|
||||||
|
Assert.assertTrue(charges.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Charge-Unteroptionen bleiben funktionsfähig
|
||||||
|
@Test
|
||||||
|
public void test10_ChargeSubOptionsRemainFunctional()
|
||||||
|
{
|
||||||
|
configStore.put("itemInformation", true);
|
||||||
|
configStore.put("itemInformationCharges", true);
|
||||||
|
configStore.put("itemInformationChargesWeapons", false);
|
||||||
|
configStore.put("itemInformationChargesBags", true);
|
||||||
|
|
||||||
|
Assert.assertTrue(itemCharges.isEnabled());
|
||||||
|
List<ItemCharges.ChargeValue> weaponCharges = itemCharges.chargesFor(ItemID.ABYSSAL_TENTACLE);
|
||||||
|
Assert.assertTrue(weaponCharges.isEmpty());
|
||||||
|
|
||||||
|
List<ItemCharges.ChargeValue> bagCharges = itemCharges.chargesFor(ItemID.GEM_BAG);
|
||||||
|
Assert.assertFalse(bagCharges.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. alte Konfigurationswerte werden korrekt übernommen
|
||||||
|
@Test
|
||||||
|
public void test11_LegacyConfigFallback()
|
||||||
|
{
|
||||||
|
configStore.clear();
|
||||||
|
configStore.put("itemPrices", true);
|
||||||
|
configStore.put("itemPricesGe", false);
|
||||||
|
|
||||||
|
Assert.assertTrue(plugin.isItemInformationEnabled());
|
||||||
|
boolean geVal = plugin.getHelperValue("itemInformationGe", Boolean.class, true);
|
||||||
|
Assert.assertFalse(geVal);
|
||||||
|
|
||||||
|
configStore.clear();
|
||||||
|
configStore.put("itemCharges", true);
|
||||||
|
configStore.put("itemChargesWeapons", false);
|
||||||
|
|
||||||
|
Assert.assertTrue(plugin.isItemInformationEnabled());
|
||||||
|
Assert.assertTrue(itemCharges.isEnabled());
|
||||||
|
boolean weaponsVal = plugin.getHelperValue("itemInformationChargesWeapons", Boolean.class, true);
|
||||||
|
Assert.assertFalse(weaponsVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Tooltip erscheint nicht bei Coins/Platinum
|
||||||
|
@Test
|
||||||
|
public void test12_CoinsAndPlatinumIgnored()
|
||||||
|
{
|
||||||
|
configStore.put("itemInformation", true);
|
||||||
|
|
||||||
|
Widget coinSlot = mockInterface(Widget.class, (p, m, a) -> {
|
||||||
|
if ("getId".equals(m.getName())) return 149 << 16;
|
||||||
|
if ("getItemId".equals(m.getName())) return 995;
|
||||||
|
if ("getItemQuantity".equals(m.getName())) return 10000;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
MenuEntry coinEntry = mockInterface(MenuEntry.class, (p, m, a) -> {
|
||||||
|
if ("getWidget".equals(m.getName())) return coinSlot;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
currentMenuEntries = new MenuEntry[]{coinEntry};
|
||||||
|
|
||||||
|
overlay.render(null);
|
||||||
|
Assert.assertNull(lastTooltip());
|
||||||
|
|
||||||
|
Widget platSlot = mockInterface(Widget.class, (p, m, a) -> {
|
||||||
|
if ("getId".equals(m.getName())) return 149 << 16;
|
||||||
|
if ("getItemId".equals(m.getName())) return ItemID.PLATINUM;
|
||||||
|
if ("getItemQuantity".equals(m.getName())) return 50;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
MenuEntry platEntry = mockInterface(MenuEntry.class, (p, m, a) -> {
|
||||||
|
if ("getWidget".equals(m.getName())) return platSlot;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
currentMenuEntries = new MenuEntry[]{platEntry};
|
||||||
|
|
||||||
|
overlay.render(null);
|
||||||
|
Assert.assertNull(lastTooltip());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package com.example.essentialsplus;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.ui.TimeFilter;
|
||||||
|
import com.example.essentialsplus.ui.TimeRange;
|
||||||
|
import com.example.essentialsplus.ui.Ui;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class TimeRangeTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void testDefaults()
|
||||||
|
{
|
||||||
|
assertEquals("Now", TimeRange.CURRENT.getLabel());
|
||||||
|
assertEquals("Sess", TimeRange.SESSION.getLabel());
|
||||||
|
assertEquals("1H", TimeRange.HOUR.getLabel());
|
||||||
|
assertEquals("1D", TimeRange.DAY.getLabel());
|
||||||
|
assertEquals("1W", TimeRange.WEEK.getLabel());
|
||||||
|
assertEquals("1M", TimeRange.MONTH.getLabel());
|
||||||
|
assertEquals("All", TimeRange.ALL.getLabel());
|
||||||
|
|
||||||
|
assertEquals(7, TimeRange.values().length);
|
||||||
|
assertTrue(TimeRange.CURRENT.isCurrent());
|
||||||
|
assertTrue(TimeRange.SESSION.isSession());
|
||||||
|
assertTrue(TimeRange.ALL.isAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseCustomWeeks()
|
||||||
|
{
|
||||||
|
TimeRange r1w = TimeRange.parseCustom("1w");
|
||||||
|
assertNotNull(r1w);
|
||||||
|
assertEquals("1W", r1w.getLabel());
|
||||||
|
assertEquals(7 * Ui.DAY, r1w.getMillis());
|
||||||
|
|
||||||
|
TimeRange r2w = TimeRange.parseCustom("2w");
|
||||||
|
assertNotNull(r2w);
|
||||||
|
assertEquals("2W", r2w.getLabel());
|
||||||
|
assertEquals(14 * Ui.DAY, r2w.getMillis());
|
||||||
|
|
||||||
|
TimeRange r3w = TimeRange.parseCustom("3w");
|
||||||
|
assertNotNull(r3w);
|
||||||
|
assertEquals("3W", r3w.getLabel());
|
||||||
|
assertEquals(21 * Ui.DAY, r3w.getMillis());
|
||||||
|
|
||||||
|
TimeRange r5w = TimeRange.parseCustom("5w");
|
||||||
|
assertNotNull(r5w);
|
||||||
|
assertEquals("5W", r5w.getLabel());
|
||||||
|
assertEquals(35 * Ui.DAY, r5w.getMillis());
|
||||||
|
|
||||||
|
TimeRange rWeeks = TimeRange.parseCustom("4 weeks");
|
||||||
|
assertNotNull(rWeeks);
|
||||||
|
assertEquals("4W", rWeeks.getLabel());
|
||||||
|
assertEquals(28 * Ui.DAY, rWeeks.getMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseCustomDaysHoursMinutesYears()
|
||||||
|
{
|
||||||
|
TimeRange d3 = TimeRange.parseCustom("3d");
|
||||||
|
assertNotNull(d3);
|
||||||
|
assertEquals("3D", d3.getLabel());
|
||||||
|
assertEquals(3 * Ui.DAY, d3.getMillis());
|
||||||
|
|
||||||
|
TimeRange h12 = TimeRange.parseCustom("12h");
|
||||||
|
assertNotNull(h12);
|
||||||
|
assertEquals("12H", h12.getLabel());
|
||||||
|
assertEquals(12 * Ui.HOUR, h12.getMillis());
|
||||||
|
|
||||||
|
TimeRange y2 = TimeRange.parseCustom("2y");
|
||||||
|
assertNotNull(y2);
|
||||||
|
assertEquals("2Y", y2.getLabel());
|
||||||
|
assertEquals(2 * 365 * Ui.DAY, y2.getMillis());
|
||||||
|
|
||||||
|
TimeRange m30 = TimeRange.parseCustom("30m");
|
||||||
|
assertNotNull(m30);
|
||||||
|
assertEquals("30m", m30.getLabel());
|
||||||
|
assertEquals(30 * 60 * 1000L, m30.getMillis());
|
||||||
|
|
||||||
|
TimeRange mo2 = TimeRange.parseCustom("2m");
|
||||||
|
assertNotNull(mo2);
|
||||||
|
assertEquals("2M", mo2.getLabel());
|
||||||
|
assertEquals(2 * 30 * Ui.DAY, mo2.getMillis());
|
||||||
|
|
||||||
|
TimeRange mo6 = TimeRange.parseCustom("6mo");
|
||||||
|
assertNotNull(mo6);
|
||||||
|
assertEquals("6M", mo6.getLabel());
|
||||||
|
assertEquals(6 * 30 * Ui.DAY, mo6.getMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSpecialKeywords()
|
||||||
|
{
|
||||||
|
assertEquals(TimeRange.CURRENT, TimeRange.parseCustom("now"));
|
||||||
|
assertEquals(TimeRange.SESSION, TimeRange.parseCustom("sess"));
|
||||||
|
assertEquals(TimeRange.SESSION, TimeRange.parseCustom("session"));
|
||||||
|
assertEquals(TimeRange.ALL, TimeRange.parseCustom("all"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidInputs()
|
||||||
|
{
|
||||||
|
assertNull(TimeRange.parseCustom(null));
|
||||||
|
assertNull(TimeRange.parseCustom(""));
|
||||||
|
assertNull(TimeRange.parseCustom(" "));
|
||||||
|
assertNull(TimeRange.parseCustom("abc"));
|
||||||
|
assertNull(TimeRange.parseCustom("-5d"));
|
||||||
|
assertNull(TimeRange.parseCustom("0h"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseSerialization()
|
||||||
|
{
|
||||||
|
TimeRange r2w = TimeRange.custom("2W", 14 * Ui.DAY);
|
||||||
|
String serialized = r2w.name();
|
||||||
|
TimeRange restored = TimeRange.parse(serialized);
|
||||||
|
assertEquals(r2w, restored);
|
||||||
|
assertEquals("2W", restored.getLabel());
|
||||||
|
assertEquals(14 * Ui.DAY, restored.getMillis());
|
||||||
|
|
||||||
|
assertEquals(TimeRange.WEEK, TimeRange.parse("TWO_WEEKS"));
|
||||||
|
assertEquals(TimeRange.MONTH, TimeRange.parse("QUARTER"));
|
||||||
|
assertEquals(TimeRange.HOUR, TimeRange.parse("HOUR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTimeFilterCustomSlots()
|
||||||
|
{
|
||||||
|
AtomicReference<String> savedSlots = new AtomicReference<>();
|
||||||
|
TimeFilter filter = new TimeFilter(TimeRange.DAY, null, null, savedSlots::set);
|
||||||
|
|
||||||
|
assertEquals(7, filter.getSlots().size());
|
||||||
|
assertEquals(TimeRange.CURRENT, filter.getSlots().get(0));
|
||||||
|
assertEquals(TimeRange.SESSION, filter.getSlots().get(1));
|
||||||
|
assertEquals(TimeRange.HOUR, filter.getSlots().get(2));
|
||||||
|
assertEquals(TimeRange.DAY, filter.getSlots().get(3));
|
||||||
|
assertEquals(TimeRange.WEEK, filter.getSlots().get(4));
|
||||||
|
assertEquals(TimeRange.MONTH, filter.getSlots().get(5));
|
||||||
|
assertEquals(TimeRange.ALL, filter.getSlots().get(6));
|
||||||
|
|
||||||
|
assertEquals(3, filter.getSlotIndex(TimeRange.DAY));
|
||||||
|
|
||||||
|
// Customize slot 2 (1H) to 2W
|
||||||
|
TimeRange r2w = TimeRange.custom("2W", 14 * Ui.DAY);
|
||||||
|
filter.setSlot(2, r2w);
|
||||||
|
|
||||||
|
assertEquals(r2w, filter.getSlots().get(2));
|
||||||
|
assertEquals(2, filter.getSlotIndex(r2w));
|
||||||
|
assertNotNull(savedSlots.get());
|
||||||
|
assertTrue(savedSlots.get().contains("2W"));
|
||||||
|
|
||||||
|
// Filter calculation using 2W
|
||||||
|
filter.set(r2w);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long start = filter.start();
|
||||||
|
assertTrue(start <= now - 14 * Ui.DAY && start >= now - 14 * Ui.DAY - 5000);
|
||||||
|
assertEquals("in 2W", filter.inLabel());
|
||||||
|
|
||||||
|
// Reset slot 2
|
||||||
|
filter.resetSlot(2);
|
||||||
|
assertEquals(TimeRange.HOUR, filter.getSlots().get(2));
|
||||||
|
|
||||||
|
// Reset all slots
|
||||||
|
filter.setSlot(0, r2w);
|
||||||
|
filter.resetSlots();
|
||||||
|
assertEquals(TimeRange.CURRENT, filter.getSlots().get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRestoreSlotsFromConfig()
|
||||||
|
{
|
||||||
|
String configStr = "CURRENT,SESSION,CUSTOM:5W:3024000000,DAY,WEEK,CUSTOM:2Y:63072000000,ALL";
|
||||||
|
TimeFilter filter = new TimeFilter(TimeRange.DAY, null, configStr, null);
|
||||||
|
|
||||||
|
assertEquals(7, filter.getSlots().size());
|
||||||
|
assertEquals("5W", filter.getSlots().get(2).getLabel());
|
||||||
|
assertEquals(3024000000L, filter.getSlots().get(2).getMillis());
|
||||||
|
assertEquals("2Y", filter.getSlots().get(5).getLabel());
|
||||||
|
assertEquals(63072000000L, filter.getSlots().get(5).getMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -235,4 +235,48 @@ public class WealthSpikeFilterTest
|
|||||||
Assert.assertEquals(100_000_000L, clean.get(0).total());
|
Assert.assertEquals(100_000_000L, clean.get(0).total());
|
||||||
Assert.assertEquals(102_000_000L, clean.get(1).total());
|
Assert.assertEquals(102_000_000L, clean.get(1).total());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPlateauSpikeIsRemoved()
|
||||||
|
{
|
||||||
|
// 154.7M baseline -> 18 points @ 161.2M (~18 mins) -> 154.9M baseline
|
||||||
|
List<Snapshot> raw = new ArrayList<>();
|
||||||
|
long t = 1000;
|
||||||
|
raw.add(snap(t, 154_700_000L));
|
||||||
|
for (int i = 0; i < 18; i++)
|
||||||
|
{
|
||||||
|
t += 60_000;
|
||||||
|
raw.add(snap(t, 161_200_000L + (i * 10_000L)));
|
||||||
|
}
|
||||||
|
t += 60_000;
|
||||||
|
raw.add(snap(t, 154_900_000L));
|
||||||
|
|
||||||
|
Assert.assertEquals(20, raw.size());
|
||||||
|
List<Snapshot> clean = AccountData.sanitizeHistory(raw);
|
||||||
|
|
||||||
|
Assert.assertEquals(2, clean.size());
|
||||||
|
Assert.assertEquals(154_700_000L, clean.get(0).total());
|
||||||
|
Assert.assertEquals(154_900_000L, clean.get(1).total());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLoginStepGlitchIsRemoved()
|
||||||
|
{
|
||||||
|
// 161.8M yesterday -> 10.7h gap -> 162.2M on login (102s unverified cache) -> 155.7M on bank sync -> 155.8M sustained
|
||||||
|
List<Snapshot> raw = new ArrayList<>();
|
||||||
|
raw.add(snap(1000, 161_856_503L));
|
||||||
|
long loginTime = 1000 + 10 * 3600 * 1000L;
|
||||||
|
raw.add(snap(loginTime, 162_198_011L)); // unverified bank cache artifact
|
||||||
|
raw.add(snap(loginTime + 102_000L, 155_767_415L)); // verified bank sync
|
||||||
|
raw.add(snap(loginTime + 163_000L, 155_807_432L)); // sustained session
|
||||||
|
raw.add(snap(loginTime + 223_000L, 155_806_232L));
|
||||||
|
|
||||||
|
List<Snapshot> clean = AccountData.sanitizeHistory(raw);
|
||||||
|
|
||||||
|
Assert.assertEquals(4, clean.size());
|
||||||
|
Assert.assertEquals(161_856_503L, clean.get(0).total());
|
||||||
|
Assert.assertEquals(155_767_415L, clean.get(1).total());
|
||||||
|
Assert.assertEquals(155_807_432L, clean.get(2).total());
|
||||||
|
Assert.assertEquals(155_806_232L, clean.get(3).total());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.example.essentialsplus.helpers;
|
|||||||
|
|
||||||
import com.example.essentialsplus.EssentialsPlusConfig;
|
import com.example.essentialsplus.EssentialsPlusConfig;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.Rectangle;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -13,10 +14,19 @@ import net.runelite.api.ChatMessageType;
|
|||||||
import net.runelite.api.Client;
|
import net.runelite.api.Client;
|
||||||
import net.runelite.api.IterableHashTable;
|
import net.runelite.api.IterableHashTable;
|
||||||
import net.runelite.api.MessageNode;
|
import net.runelite.api.MessageNode;
|
||||||
|
import net.runelite.api.Menu;
|
||||||
|
import net.runelite.api.MenuAction;
|
||||||
|
import net.runelite.api.MenuEntry;
|
||||||
|
import net.runelite.api.Point;
|
||||||
|
import net.runelite.api.ScriptID;
|
||||||
import net.runelite.api.Player;
|
import net.runelite.api.Player;
|
||||||
import net.runelite.api.events.ChatMessage;
|
import net.runelite.api.events.ChatMessage;
|
||||||
import net.runelite.api.events.GameTick;
|
import net.runelite.api.events.GameTick;
|
||||||
|
import net.runelite.api.events.MenuEntryAdded;
|
||||||
|
import net.runelite.api.events.MenuOpened;
|
||||||
import net.runelite.api.events.ScriptCallbackEvent;
|
import net.runelite.api.events.ScriptCallbackEvent;
|
||||||
|
import net.runelite.api.gameval.InterfaceID;
|
||||||
|
import net.runelite.api.widgets.Widget;
|
||||||
import net.runelite.client.callback.ClientThread;
|
import net.runelite.client.callback.ClientThread;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
@@ -36,6 +46,71 @@ public class ChatHelperTest
|
|||||||
private int[] intStack;
|
private int[] intStack;
|
||||||
private Object[] objStack;
|
private Object[] objStack;
|
||||||
private Player localPlayer;
|
private Player localPlayer;
|
||||||
|
private final List<MenuEntry> menuEntries = new ArrayList<>();
|
||||||
|
private final Map<Integer, Widget> widgets = new HashMap<>();
|
||||||
|
private Point mouse = new Point(50, 420);
|
||||||
|
private String copiedText;
|
||||||
|
private int chatRefreshes;
|
||||||
|
|
||||||
|
private MenuEntry entry(String option, MenuAction type, int param1)
|
||||||
|
{
|
||||||
|
Map<String, Object> values = new HashMap<>();
|
||||||
|
values.put("Option", option);
|
||||||
|
values.put("Type", type);
|
||||||
|
values.put("Param1", param1);
|
||||||
|
return mockInterface(MenuEntry.class, (proxy, method, args) ->
|
||||||
|
{
|
||||||
|
String name = method.getName();
|
||||||
|
if (name.startsWith("set"))
|
||||||
|
{
|
||||||
|
values.put(name.substring(3), args[0]);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
if (name.equals("onClick"))
|
||||||
|
{
|
||||||
|
if (args == null || args.length == 0) return values.get("OnClick");
|
||||||
|
values.put("OnClick", args[0]);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
if (name.equals("getWidget"))
|
||||||
|
{
|
||||||
|
return widgets.get(values.get("Param1"));
|
||||||
|
}
|
||||||
|
return name.startsWith("get") ? values.get(name.substring(3)) : null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Widget widget(int id, String text, Rectangle bounds, boolean hidden, Widget parent, Widget[] children)
|
||||||
|
{
|
||||||
|
Widget widget = mockInterface(Widget.class, (proxy, method, args) ->
|
||||||
|
{
|
||||||
|
switch (method.getName())
|
||||||
|
{
|
||||||
|
case "getId": return id;
|
||||||
|
case "getText": return text;
|
||||||
|
case "getBounds": return bounds;
|
||||||
|
case "isHidden": return hidden;
|
||||||
|
case "getParent": return parent;
|
||||||
|
case "getChildren": return children;
|
||||||
|
case "getChild":
|
||||||
|
int index = (Integer) args[0];
|
||||||
|
return index >= 0 && index < children.length ? children[index] : null;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
widgets.put(id, widget);
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Widget[] chatWidgets(boolean hidden)
|
||||||
|
{
|
||||||
|
Widget[] children = new Widget[8];
|
||||||
|
Widget scroll = widget(InterfaceID.Chatbox.SCROLLAREA, "", new Rectangle(0, 400, 500, 100), hidden, null, children);
|
||||||
|
children[1] = widget(-1, "First message", new Rectangle(20, 440, 300, 15), false, scroll, null);
|
||||||
|
children[5] = widget(-2, "<col=ffffff>You smelt a bar.</col>", new Rectangle(20, 410, 300, 20), false, scroll, null);
|
||||||
|
widget(InterfaceID.Chatbox.LINE0 + 1, "Player", new Rectangle(), false, scroll, null);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static <T> T mockInterface(Class<T> iface, java.lang.reflect.InvocationHandler handler)
|
private static <T> T mockInterface(Class<T> iface, java.lang.reflect.InvocationHandler handler)
|
||||||
@@ -105,6 +180,10 @@ public class ChatHelperTest
|
|||||||
scriptsRun.clear();
|
scriptsRun.clear();
|
||||||
configValues.clear();
|
configValues.clear();
|
||||||
messagesMap.clear();
|
messagesMap.clear();
|
||||||
|
menuEntries.clear();
|
||||||
|
widgets.clear();
|
||||||
|
chatRefreshes = 0;
|
||||||
|
copiedText = null;
|
||||||
|
|
||||||
// Default config values
|
// Default config values
|
||||||
configValues.put("chatHelper", true);
|
configValues.put("chatHelper", true);
|
||||||
@@ -157,10 +236,32 @@ public class ChatHelperTest
|
|||||||
{
|
{
|
||||||
switch (method.getName())
|
switch (method.getName())
|
||||||
{
|
{
|
||||||
|
case "refreshChat":
|
||||||
|
chatRefreshes++;
|
||||||
|
return null;
|
||||||
|
case "getMouseCanvasPosition": return mouse;
|
||||||
|
case "getWidget":
|
||||||
|
return widgets.get(args.length == 1 ? (Integer) args[0] : ((Integer) args[0] << 16) | (Integer) args[1]);
|
||||||
|
case "getMenu":
|
||||||
|
return mockInterface(Menu.class, (p, m, a) ->
|
||||||
|
{
|
||||||
|
if (m.getName().equals("getMenuEntries")) return menuEntries.toArray(new MenuEntry[0]);
|
||||||
|
if (m.getName().equals("setMenuEntries"))
|
||||||
|
{
|
||||||
|
menuEntries.clear();
|
||||||
|
java.util.Collections.addAll(menuEntries, (MenuEntry[]) a[0]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
case "createMenuEntry":
|
||||||
|
MenuEntry entry = entry("", MenuAction.RUNELITE, 0);
|
||||||
|
int index = (Integer) args[0];
|
||||||
|
menuEntries.add(index < 0 ? menuEntries.size() + index + 1 : index, entry);
|
||||||
|
return entry;
|
||||||
case "getChatLineMap":
|
case "getChatLineMap":
|
||||||
return chatLineMap;
|
return chatLineMap;
|
||||||
case "runScript":
|
case "runScript":
|
||||||
scriptsRun.add((Object[]) args);
|
scriptsRun.add((Object[]) args[0]);
|
||||||
return null;
|
return null;
|
||||||
case "getLocalPlayer":
|
case "getLocalPlayer":
|
||||||
return localPlayer;
|
return localPlayer;
|
||||||
@@ -186,7 +287,14 @@ public class ChatHelperTest
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
chatHelper = new ChatHelper(client, config, clientThread);
|
chatHelper = new ChatHelper(client, config, clientThread)
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
void copyToClipboard(String text)
|
||||||
|
{
|
||||||
|
copiedText = text;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -207,6 +315,100 @@ public class ChatHelperTest
|
|||||||
Assert.assertEquals(1, buf.getLines().length);
|
Assert.assertEquals(1, buf.getLines().length);
|
||||||
Assert.assertEquals("You cut an oak log.", buf.getLines()[0].getValue());
|
Assert.assertEquals("You cut an oak log.", buf.getLines()[0].getValue());
|
||||||
Assert.assertFalse(scriptsRun.isEmpty());
|
Assert.assertFalse(scriptsRun.isEmpty());
|
||||||
|
Assert.assertEquals(1, chatRefreshes);
|
||||||
|
Assert.assertEquals(ScriptID.SPLITPM_CHANGED, scriptsRun.get(0)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clearsWelcomeAndSystemMessagesButProtectsClanMessages()
|
||||||
|
{
|
||||||
|
int old = (int) Instant.now().getEpochSecond() - 120;
|
||||||
|
for (ChatMessageType type : new ChatMessageType[]{ChatMessageType.WELCOME, ChatMessageType.PLAYERRELATED,
|
||||||
|
ChatMessageType.CLAN_GUEST_MESSAGE, ChatMessageType.FRIENDSCHATNOTIFICATION})
|
||||||
|
{
|
||||||
|
chatLineMap.put(type.getType(), createChatLineBuffer(List.of(createMessageNode(type.getType(), type, "", "message", old))));
|
||||||
|
}
|
||||||
|
chatHelper.onGameTick(new GameTick());
|
||||||
|
Assert.assertEquals(0, chatLineMap.get(ChatMessageType.WELCOME.getType()).getLength());
|
||||||
|
Assert.assertEquals(0, chatLineMap.get(ChatMessageType.PLAYERRELATED.getType()).getLength());
|
||||||
|
Assert.assertEquals(1, chatLineMap.get(ChatMessageType.CLAN_GUEST_MESSAGE.getType()).getLength());
|
||||||
|
Assert.assertEquals(1, chatLineMap.get(ChatMessageType.FRIENDSCHATNOTIFICATION.getType()).getLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void autoClearRespectsDisabledSettings()
|
||||||
|
{
|
||||||
|
int old = (int) Instant.now().getEpochSecond() - 120;
|
||||||
|
chatLineMap.put(0, createChatLineBuffer(List.of(createMessageNode(1, ChatMessageType.GAMEMESSAGE, "", "old", old))));
|
||||||
|
configValues.put("chatAutoClearTime", 0);
|
||||||
|
chatHelper.onGameTick(new GameTick());
|
||||||
|
configValues.put("chatAutoClearTime", 60);
|
||||||
|
configValues.put("chatAutoClear", false);
|
||||||
|
chatHelper.onGameTick(new GameTick());
|
||||||
|
configValues.put("chatAutoClear", true);
|
||||||
|
configValues.put("chatHelper", false);
|
||||||
|
chatHelper.onGameTick(new GameTick());
|
||||||
|
Assert.assertEquals(1, chatLineMap.get(0).getLength());
|
||||||
|
Assert.assertEquals(0, chatRefreshes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyUsesMessageLineOffsetAndCapturesText()
|
||||||
|
{
|
||||||
|
Widget[] children = chatWidgets(false);
|
||||||
|
MenuEntry report = entry("Report", MenuAction.CC_OP, InterfaceID.Chatbox.LINE0 + 1);
|
||||||
|
menuEntries.add(report);
|
||||||
|
chatHelper.onMenuEntryAdded(new MenuEntryAdded(report));
|
||||||
|
Assert.assertEquals(2, menuEntries.size());
|
||||||
|
MenuEntry copy = menuEntries.get(1);
|
||||||
|
Assert.assertEquals("Copy message", copy.getOption());
|
||||||
|
children[5] = null;
|
||||||
|
copy.onClick().accept(copy);
|
||||||
|
Assert.assertEquals("<col=ffffff>You smelt a bar.</col>", copiedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyWorksOnClickThroughGameMessageWithoutReport()
|
||||||
|
{
|
||||||
|
chatWidgets(false);
|
||||||
|
menuEntries.add(entry("Cancel", MenuAction.CANCEL, 0));
|
||||||
|
menuEntries.add(entry("Walk here", MenuAction.WALK, 0));
|
||||||
|
chatHelper.onMenuOpened(new MenuOpened());
|
||||||
|
Assert.assertEquals(2, menuEntries.size());
|
||||||
|
MenuEntry copy = menuEntries.get(1);
|
||||||
|
Assert.assertEquals("Copy message", copy.getOption());
|
||||||
|
copy.onClick().accept(copy);
|
||||||
|
Assert.assertEquals("<col=ffffff>You smelt a bar.</col>", copiedText);
|
||||||
|
chatHelper.onMenuOpened(new MenuOpened());
|
||||||
|
Assert.assertEquals(2, menuEntries.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyIgnoresHiddenClippedAndUnrelatedWidgets()
|
||||||
|
{
|
||||||
|
menuEntries.add(entry("Walk here", MenuAction.WALK, 0));
|
||||||
|
chatWidgets(true);
|
||||||
|
chatHelper.onMenuOpened(new MenuOpened());
|
||||||
|
Assert.assertEquals(1, menuEntries.size());
|
||||||
|
chatWidgets(false);
|
||||||
|
mouse = new Point(50, 390);
|
||||||
|
chatHelper.onMenuOpened(new MenuOpened());
|
||||||
|
Assert.assertEquals(1, menuEntries.size());
|
||||||
|
mouse = new Point(50, 420);
|
||||||
|
widget(12 << 16, "Bank", new Rectangle(), false, null, null);
|
||||||
|
menuEntries.add(entry("Withdraw", MenuAction.CC_OP, 12 << 16));
|
||||||
|
chatHelper.onMenuOpened(new MenuOpened());
|
||||||
|
Assert.assertEquals(2, menuEntries.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void copyRespectsDisabledSetting()
|
||||||
|
{
|
||||||
|
chatWidgets(false);
|
||||||
|
configValues.put("chatCopyToClipboard", false);
|
||||||
|
chatHelper.onMenuOpened(new MenuOpened());
|
||||||
|
chatHelper.onMenuEntryAdded(new MenuEntryAdded(entry("Report", MenuAction.CC_OP, InterfaceID.Chatbox.LINE0 + 1)));
|
||||||
|
Assert.assertTrue(menuEntries.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.example.essentialsplus.ui;
|
||||||
|
|
||||||
|
import com.example.essentialsplus.WealthCategory;
|
||||||
|
import com.example.essentialsplus.data.AccountData;
|
||||||
|
import com.example.essentialsplus.data.Snapshot;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class SeriesTest
|
||||||
|
{
|
||||||
|
private static Snapshot snap(long t, long total)
|
||||||
|
{
|
||||||
|
long[] v = new long[WealthCategory.COUNT];
|
||||||
|
v[WealthCategory.BANK.ordinal()] = total;
|
||||||
|
return new Snapshot(t, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCombineCarriesEarliestKnownPointBackwardsWithoutSpike()
|
||||||
|
{
|
||||||
|
// Series 1 starts at t=100 with 7.8M
|
||||||
|
List<Snapshot> s1 = new ArrayList<>();
|
||||||
|
s1.add(snap(100, 7_800_000L));
|
||||||
|
s1.add(snap(200, 7_800_000L));
|
||||||
|
|
||||||
|
// Series 2 starts at t=110 with 162M (e.g. slight timestamp offset on login)
|
||||||
|
List<Snapshot> s2 = new ArrayList<>();
|
||||||
|
s2.add(snap(110, 162_000_000L));
|
||||||
|
s2.add(snap(200, 162_000_000L));
|
||||||
|
|
||||||
|
List<Snapshot> combined = Series.combine(List.of(s1, s2));
|
||||||
|
|
||||||
|
// Point at t=100 must include s2's initial value (162M), not 0!
|
||||||
|
Assert.assertEquals(3, combined.size());
|
||||||
|
Assert.assertEquals(100L, combined.get(0).t);
|
||||||
|
Assert.assertEquals(169_800_000L, combined.get(0).total());
|
||||||
|
Assert.assertEquals(110L, combined.get(1).t);
|
||||||
|
Assert.assertEquals(169_800_000L, combined.get(1).total());
|
||||||
|
Assert.assertEquals(200L, combined.get(2).t);
|
||||||
|
Assert.assertEquals(169_800_000L, combined.get(2).total());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testForAccountDoesNotCarryOldLogoutValuesAcrossInactivityGaps()
|
||||||
|
{
|
||||||
|
AccountData acc = new AccountData();
|
||||||
|
acc.bankKnown = true;
|
||||||
|
acc.history.add(snap(1000, 161_800_000L)); // yesterday before logout
|
||||||
|
|
||||||
|
long sessionStart = 1000 + 10 * 3600 * 1000L; // 10 hours later
|
||||||
|
long firstVerifiedBank = sessionStart + 100_000L;
|
||||||
|
acc.history.add(snap(firstVerifiedBank, 155_800_000L));
|
||||||
|
|
||||||
|
List<Snapshot> series = Series.forAccount(acc, sessionStart, firstVerifiedBank + 100_000L);
|
||||||
|
|
||||||
|
// The baseline at sessionStart must be the first verified value in this session (155.8M),
|
||||||
|
// NOT yesterday's pre-logout value (161.8M).
|
||||||
|
Assert.assertEquals(sessionStart, series.get(0).t);
|
||||||
|
Assert.assertEquals(155_800_000L, series.get(0).total());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
# Tree Cooldowns – Taskbeschreibung
|
|
||||||
|
|
||||||
Implementiere ein **„Tree Cooldowns"**-Helper in **Essentials Plus** (`com.example.essentialsplus.helpers`). Es gibt zwei Anzeigen:
|
|
||||||
|
|
||||||
1. **Respawn-Countdown** über Baumstümpfen: Sekunden bis der Baum wieder wächst (nur Stümpfe, deren Fällung man in der Nähe *miterlebt* hat — zugehörige Zeiten sind sonst nicht messbar).
|
|
||||||
2. **Verbleibende Chop-Zeit** auf langlebigen Bäumen (Maple+, z. B. Yew/Magic/Redwood) über dem Baum, solange man ihn hackt.
|
|
||||||
|
|
||||||
**Format:** reine Sekunden (Ganzzahl, z. B. `8`). Deutsch als Display-Sprache.
|
|
||||||
|
|
||||||
## Mechanik (OSRS-Wiki, Depletion-System)
|
|
||||||
|
|
||||||
Despawn = wie lange aktiv gehäckt werden kann (beginnt beim ersten Hieb, **regeneriert** bei Pause). Respawn = Zeit bis der Baum wieder da ist.
|
|
||||||
|
|
||||||
| Baum | Despawn | Respawn |
|
|
||||||
|---|---|---|
|
|
||||||
| Normal tree | — | ~10 s |
|
|
||||||
| Oak | 27 s | 8,4 s |
|
|
||||||
| Willow | 30 s | 8,4 s |
|
|
||||||
| Teak | 30 s | 9 s |
|
|
||||||
| Maple | 60 s | 35,4 s |
|
|
||||||
| Hollow | 36 s | 25,8 s |
|
|
||||||
| Mahogany | 60 s | 8,4 s |
|
|
||||||
| Arctic pine | 84 s | 8,4 s |
|
|
||||||
| Yew | 114 s | 59,4 s |
|
|
||||||
| Camphor | 120 s | 59,4 s |
|
|
||||||
| Magic | 234 s | 119,4 s |
|
|
||||||
| Ironwood | 240 s | 119,4 s |
|
|
||||||
| Redwood | 264 s | 119,4 s |
|
|
||||||
| Rosewood | 275 s | 123 s |
|
|
||||||
|
|
||||||
Chop-Countdown nur wenn `despawnSec > 30` (`CHOP_MIN_DESPAWN_SEC`).
|
|
||||||
|
|
||||||
## Neue Dateien
|
|
||||||
|
|
||||||
### `helpers/TreeCooldowns.java` (`@Singleton`, Logik)
|
|
||||||
|
|
||||||
- **`TreeType`-Enum** mit `despawnSec`, `respawnSec` und Objekt-ID-Sets aus `net.runelite.api.gameval.ObjectID` (z. B. `OAKTREE`, `OAKTREE_STUMP`, `WILLOWTREE`, `YEWTREE`, `YEWTREE_STUMP`, `MAPLETREE`, `MAGICTREE`, `TEAKTREE`, `MAHOGANYTREE`, `ACHEY_TREE`, `REDWOODTREE_L/_R(+_CUT)`, diverse Varianten wie `TREE/TREE2/LIGHTTREE/DEADTREE/JUNGLETREE/EVERGREEN` plus jeweilige `*_STUMP`-Ids). IDs zusätzlich über `ObjectComposition.getName()` verifizieren; unbekannte Regio-Varianten ergänzen.
|
|
||||||
- **`WOODCUT_ANIMS`**: alle `AnimationID.HUMAN_WOODCUTTING_*`.
|
|
||||||
- **State**: `Map<TileKey(sceneX, sceneY, plane), TreeState{type, choppedMs, chopStartTick, respawnDeadlineMs}>`.
|
|
||||||
- **Events** (im Plugin registriert):
|
|
||||||
- `GameTick`: Ist `client.getLocalPlayer().getAnimation()` ∈ `WOODCUT_ANIMS` → nächsten Baum in Reichweite suchen (Scan der Scene-Tiles wie `WorldObjectHighlighterOverlay.java:91`), als Ziel merken und `choppedMs += 600`. Kein Hacken (Animation weg / anderes Ziel) → `choppedMs` zurücksetzen (Depletion regeneriert).
|
|
||||||
- `GameObjectDespawned`: getracktes Ziel (Baum) verschwindet → felled: `respawnDeadlineMs = now + respawnSec(type)` am Tile setzen.
|
|
||||||
- `GameObjectSpawned`:
|
|
||||||
- Stump-Id → fremder Baum wurde in der Nähe gefällt → ebenfalls `respawnDeadlineMs = now + respawnSec(stumpType)` setzen (Fallback `TreeType.NORMAL` bei unbekannter Stump-Id). Nur wenn Tile in einem Radius von ~16 Tiles um den Spieler („in der Nähe gefällt").
|
|
||||||
- Baum-Id an einem Tile mit aktiver Respawn-Deadline → Eintrag löschen (wiedergewachsen).
|
|
||||||
- `GameStateChanged` (`!= LOGGED_IN`) und Region-Wechsel → `reset()`.
|
|
||||||
- **API für das Overlay**: Iteratoren `respawnTimers()`, `chopTimers()` (nur Records mit Rest > 0) samt Referenz auf das Tile (`LocalPoint`, `sceneX/Y`, plane).
|
|
||||||
|
|
||||||
### `helpers/TreeCooldownOverlay.java` (`Overlay`)
|
|
||||||
|
|
||||||
- `OverlayPosition.DYNAMIC`, `OverlayLayer.ABOVE_SCENE`.
|
|
||||||
- Text-Draw mit Schatten, identisch zu `AttackTimerOverlay.shadowed`; Font über `TimerTextSize`.
|
|
||||||
- **Respawn**: Sekunden-Zahl über dem Stumpf/Perspektivenpunkt der Tree-Basis (Anchor über `Perspective.getCanvasTextLocation`, s. `AttackTimerOverlay.java:130`), Höhe = `treeCooldownOffset`.
|
|
||||||
- **Chop**: Rest-Sekunden über dem Baum, Höhe = `treeCooldownOffset + 80`.
|
|
||||||
- Optional Fortschrittsbalken (`treeCooldownBar`), Muster aus `AttackTimerOverlay.drawTimer` (drainende Pips).
|
|
||||||
- Nichts rendern wenn `!config.treeCooldowns()`.
|
|
||||||
|
|
||||||
## Zu ändernde Dateien
|
|
||||||
|
|
||||||
- **`EssentialsPlusConfig.java`**: `@ConfigItem treeCooldowns` (bool, default `false`, position 34). Stil/Toggles als `configManager`-Keys via `setHelperOption` (eingesetztes Muster):
|
|
||||||
- `treeCooldownRespawn` (bool, true), `treeCooldownChop` (bool, true), `treeCooldownBar` (bool, false)
|
|
||||||
- `treeCooldownSize` (`TimerTextSize`, NORMAL), `treeCooldownOffset` (int, 50), `treeCooldownColor` (Color, weiß oder grün `0x8FBF3F`), `treeCooldownReadyColor` (Color, `0x66FF66`)
|
|
||||||
- **`EssentialsPlusPlugin.java`**: `@Inject TreeCooldowns` + `TreeCooldownOverlay`; `startUp()`: `eventBus.register(treeCooldowns)`, `overlayManager.add(treeCooldownOverlay)`; `shutDown()`: spiegeln + `treeCooldowns.reset()`. `isHelperOption()`: `case "treeCooldowns"`.
|
|
||||||
- **`HelpersCard.java`**: neues `JCheckBox "Tree cooldowns"` (Tooltip: „Respawn-Countdown auf gefällten Bäumen + Rest-Zeit auf langlebigen Bäumen. Rechtsklick für Optionen"). Rechtsklick-Menü analog `priceMenu`/`styleMenu`: Toggles `treeCooldownRespawn`/`treeCooldownChop`/`treeCooldownBar`, Größe, Höhe, Farben, Reset. `refresh()` erweitern.
|
|
||||||
|
|
||||||
## Verifikation
|
|
||||||
|
|
||||||
- `./gradlew compileJava` (bzw. `./gradlew deploy` für Bolt).
|
|
||||||
- Manuell: Baum fällen (Oak/Willow → Respawn-Zahl auf Stump), Yew/Magic/Redwood hacken (Chop-Rest über Baum, zählt runter, fällt → Respawn-Zahl), weggehen und zurück (fremder frisch gefällter Stump in der Nähe → Timer), ausloggen (alles reset).
|
|
||||||
|
|
||||||
## Hinweise / Einschränkungen
|
|
||||||
|
|
||||||
- Nur Bäume, die selbst beobachtet gefällt wurden (eigene oder fremde in der Nähe); vorab vorhandene Stümpfe unbekannten Alters → kein Timer.
|
|
||||||
- Times anderer Spieler stimmen nicht exakt (Depletion läuft global); selbst gehackte Bäume sind exakt.
|
|
||||||
Reference in New Issue
Block a user