chore: init
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
import com.example.essentialsplus.WealthCategory;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Persisted state of one character. Collections are replaced wholesale (never mutated in place,
|
||||
* except history which is copy-on-write) so the UI and the save thread can read them safely.
|
||||
*/
|
||||
public class AccountData
|
||||
{
|
||||
public int schema = 3;
|
||||
public String key;
|
||||
public long accountHash;
|
||||
public String profile;
|
||||
public String name;
|
||||
public long lastUpdated;
|
||||
public boolean bankKnown;
|
||||
public long[] current;
|
||||
/** running sum of value changes caused only by GE price movements (held quantity x price change) */
|
||||
public long market;
|
||||
|
||||
/** Last known item stacks per source ("bank", "inventory", ...): item id -> quantity. */
|
||||
public Map<String, Map<Integer, Long>> containers = new ConcurrentHashMap<>();
|
||||
public List<SourceValue> sources = new ArrayList<>();
|
||||
public List<TopItem> topItems = new ArrayList<>();
|
||||
public List<Snapshot> history = new CopyOnWriteArrayList<>();
|
||||
|
||||
/** Grand Exchange slot state (see GrandExchangeTracker) */
|
||||
public GeSlot[] geSlots;
|
||||
/** Items known to be collected already when GE tracking starts for this character (item id -> qty). */
|
||||
public Map<Integer, Long> geSeedCollected;
|
||||
/** Items that went to the bank while it was not visible; dropped when the bank is seen again. */
|
||||
public Map<Integer, Long> bankPending = new java.util.HashMap<>();
|
||||
/** 4-hour Grand Exchange buy limit tracker: canonical item id -> GeBuyLimit */
|
||||
public Map<Integer, GeBuyLimit> geBuyLimits = new ConcurrentHashMap<>();
|
||||
/** Snapshot of Grand Exchange slots for 4h buy limit tracking, modeled after LimitKeeper */
|
||||
public GeBuyLimitSlot[] geBuyLimitSlots = new GeBuyLimitSlot[8];
|
||||
|
||||
/** Timestamp of last right-click Reset Hourly in Wealth Tracker for 'Now' */
|
||||
public long resetHourlyNowAt;
|
||||
/** Timestamp of last right-click Reset Hourly in Wealth Tracker for 'Session' */
|
||||
public long resetHourlySessionAt;
|
||||
|
||||
public static class GeBuyLimitSlot
|
||||
{
|
||||
public int itemId;
|
||||
public int price;
|
||||
public int total;
|
||||
public int quantity;
|
||||
public int spent;
|
||||
|
||||
public GeBuyLimitSlot()
|
||||
{
|
||||
}
|
||||
|
||||
public GeBuyLimitSlot(int itemId, int price, int total, int quantity, int spent)
|
||||
{
|
||||
this.itemId = itemId;
|
||||
this.price = price;
|
||||
this.total = total;
|
||||
this.quantity = quantity;
|
||||
this.spent = spent;
|
||||
}
|
||||
|
||||
public boolean isSameOffer(GeBuyLimitSlot other)
|
||||
{
|
||||
return other != null && other.itemId == itemId && other.price == price && other.total == total;
|
||||
}
|
||||
}
|
||||
|
||||
public transient long fileModified;
|
||||
|
||||
public long currentTotal()
|
||||
{
|
||||
long sum = 0;
|
||||
if (current != null)
|
||||
{
|
||||
for (long x : current)
|
||||
{
|
||||
sum += x;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
public Snapshot lastSnapshot()
|
||||
{
|
||||
List<Snapshot> h = history;
|
||||
return h.isEmpty() ? null : h.get(h.size() - 1);
|
||||
}
|
||||
|
||||
public void normalize()
|
||||
{
|
||||
containers = containers == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(containers);
|
||||
sources = sources == null ? new ArrayList<>() : sources;
|
||||
topItems = topItems == null ? new ArrayList<>() : topItems;
|
||||
bankPending = bankPending == null ? new java.util.HashMap<>() : bankPending;
|
||||
geBuyLimits = geBuyLimits == null ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(geBuyLimits);
|
||||
if (geBuyLimitSlots == null || geBuyLimitSlots.length != 8)
|
||||
{
|
||||
geBuyLimitSlots = geBuyLimitSlots == null ? new GeBuyLimitSlot[8] : java.util.Arrays.copyOf(geBuyLimitSlots, 8);
|
||||
}
|
||||
if (schema < 3)
|
||||
{
|
||||
clearBuyLimits();
|
||||
schema = 3;
|
||||
}
|
||||
List<Snapshot> h = history == null ? new ArrayList<>() : history;
|
||||
h.removeIf(s -> s == null);
|
||||
h.forEach(Snapshot::normalize);
|
||||
h.sort((a, b) -> Long.compare(a.t, b.t));
|
||||
h = sanitizeHistory(h);
|
||||
history = new CopyOnWriteArrayList<>(h);
|
||||
if (current != null && current.length < WealthCategory.COUNT)
|
||||
{
|
||||
current = java.util.Arrays.copyOf(current, WealthCategory.COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
public static final long MAX_SPIKE_WINDOW_MS = 5 * 60 * 1000L;
|
||||
public static final long MIN_SPIKE_ABS_GP = 1_000_000L;
|
||||
public static final double MIN_SPIKE_REL_RATIO = 0.08;
|
||||
|
||||
/**
|
||||
* Checks if a single snapshot at (tSpike, vSpike) represents a transient outlier spike between
|
||||
* (tBefore, vBefore) and (tAfter, vAfter) that jumped away and quickly returned to baseline.
|
||||
*/
|
||||
public static boolean isTransientSpike(long tBefore, long vBefore,
|
||||
long tSpike, long vSpike,
|
||||
long tAfter, long vAfter)
|
||||
{
|
||||
long legBefore = tSpike - tBefore;
|
||||
long legAfter = tAfter - tSpike;
|
||||
if (legBefore <= 0 || legAfter <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The spike itself must be transient:
|
||||
// Either the entire span is within MAX_SPIKE_WINDOW_MS (continuous session),
|
||||
// OR across a session/logout gap (where one leg is long), the spike only persisted for
|
||||
// a brief moment (Math.min(legBefore, legAfter) <= MAX_SPIKE_WINDOW_MS).
|
||||
boolean transientDuration = (tAfter - tBefore <= MAX_SPIKE_WINDOW_MS)
|
||||
|| (Math.min(legBefore, legAfter) <= MAX_SPIKE_WINDOW_MS);
|
||||
if (!transientDuration)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long d1 = vSpike - vBefore;
|
||||
long d2 = vAfter - vSpike;
|
||||
|
||||
// Must be opposite directions (a jump away and a return)
|
||||
if ((d1 > 0 && d2 >= 0) || (d1 < 0 && d2 <= 0) || d1 == 0 || d2 == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long absD1 = Math.abs(d1);
|
||||
long absD2 = Math.abs(d2);
|
||||
long spikeMag = Math.min(absD1, absD2);
|
||||
long baseDelta = Math.abs(vAfter - vBefore);
|
||||
|
||||
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vBefore), Math.abs(vAfter)));
|
||||
|
||||
// Magnitude check: must be at least 1M GP or at least 8% of baseline wealth
|
||||
boolean significantSpike = spikeMag >= MIN_SPIKE_ABS_GP || (double) spikeMag / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||
if (!significantSpike)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Baseline consistency check: after the spike, the value returned close to the starting baseline.
|
||||
return baseDelta <= 0.35 * spikeMag
|
||||
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two consecutive snapshots represent a 2-point transient outlier spike between
|
||||
* (tBefore, vBefore) and (tAfter, vAfter).
|
||||
*/
|
||||
public static boolean isDoubleTransientSpike(long tBefore, long vBefore,
|
||||
long tSpike1, long vSpike1,
|
||||
long tSpike2, long vSpike2,
|
||||
long tAfter, long vAfter)
|
||||
{
|
||||
long legBefore = tSpike1 - tBefore;
|
||||
long legGlitch = tSpike2 - tSpike1;
|
||||
long legAfter = tAfter - tSpike2;
|
||||
if (legBefore <= 0 || legGlitch <= 0 || legAfter <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (legGlitch > MAX_SPIKE_WINDOW_MS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean transientDuration = (tAfter - tBefore <= MAX_SPIKE_WINDOW_MS)
|
||||
|| (Math.min(legBefore, legAfter) + legGlitch <= MAX_SPIKE_WINDOW_MS);
|
||||
if (!transientDuration)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long d1_1 = vSpike1 - vBefore;
|
||||
long d1_2 = vSpike2 - vBefore;
|
||||
long d2_1 = vAfter - vSpike1;
|
||||
long d2_2 = vAfter - vSpike2;
|
||||
|
||||
// Both points must deviate in the same direction from baseline and return
|
||||
boolean bothPositive = d1_1 > 0 && d1_2 > 0 && d2_1 < 0 && d2_2 < 0;
|
||||
boolean bothNegative = d1_1 < 0 && d1_2 < 0 && d2_1 > 0 && d2_2 > 0;
|
||||
if (!bothPositive && !bothNegative)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long spikeMag = Math.min(
|
||||
Math.min(Math.abs(d1_1), Math.abs(d1_2)),
|
||||
Math.min(Math.abs(d2_1), Math.abs(d2_2))
|
||||
);
|
||||
long baseDelta = Math.abs(vAfter - vBefore);
|
||||
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vBefore), Math.abs(vAfter)));
|
||||
|
||||
boolean significantSpike = spikeMag >= MIN_SPIKE_ABS_GP || (double) spikeMag / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||
if (!significantSpike)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return baseDelta <= 0.35 * spikeMag
|
||||
|| baseDelta <= (long) (0.05 * Math.max(Math.abs(vBefore), Math.abs(vAfter)) + 500_000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if three consecutive snapshots represent a 3-point transient outlier spike between
|
||||
* (tBefore, vBefore) and (tAfter, vAfter).
|
||||
*/
|
||||
public static boolean isTripleTransientSpike(long tBefore, long vBefore,
|
||||
long tSpike1, long vSpike1,
|
||||
long tSpike2, long vSpike2,
|
||||
long tSpike3, long vSpike3,
|
||||
long tAfter, long vAfter)
|
||||
{
|
||||
long legBefore = tSpike1 - tBefore;
|
||||
long legGlitch1 = tSpike2 - tSpike1;
|
||||
long legGlitch2 = tSpike3 - tSpike2;
|
||||
long totalGlitch = tSpike3 - tSpike1;
|
||||
long legAfter = tAfter - tSpike3;
|
||||
if (legBefore <= 0 || legGlitch1 <= 0 || legGlitch2 <= 0 || legAfter <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (totalGlitch > MAX_SPIKE_WINDOW_MS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean transientDuration = (tAfter - tBefore <= MAX_SPIKE_WINDOW_MS)
|
||||
|| (Math.min(legBefore, legAfter) + totalGlitch <= MAX_SPIKE_WINDOW_MS);
|
||||
if (!transientDuration)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long d1_1 = vSpike1 - vBefore;
|
||||
long d1_2 = vSpike2 - vBefore;
|
||||
long d1_3 = vSpike3 - vBefore;
|
||||
long d2_1 = vAfter - vSpike1;
|
||||
long d2_2 = vAfter - vSpike2;
|
||||
long d2_3 = vAfter - vSpike3;
|
||||
|
||||
boolean allPositive = d1_1 > 0 && d1_2 > 0 && d1_3 > 0 && d2_1 < 0 && d2_2 < 0 && d2_3 < 0;
|
||||
boolean allNegative = d1_1 < 0 && d1_2 < 0 && d1_3 < 0 && d2_1 > 0 && d2_2 > 0 && d2_3 > 0;
|
||||
if (!allPositive && !allNegative)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long spikeMag = Math.min(
|
||||
Math.min(Math.abs(d1_1), Math.min(Math.abs(d1_2), Math.abs(d1_3))),
|
||||
Math.min(Math.abs(d2_1), Math.min(Math.abs(d2_2), Math.abs(d2_3)))
|
||||
);
|
||||
long baseDelta = Math.abs(vAfter - vBefore);
|
||||
long baseWealth = Math.max(10_000L, Math.min(Math.abs(vBefore), Math.abs(vAfter)));
|
||||
|
||||
boolean significantSpike = spikeMag >= MIN_SPIKE_ABS_GP || (double) spikeMag / baseWealth >= MIN_SPIKE_REL_RATIO;
|
||||
if (!significantSpike)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return baseDelta <= 0.35 * spikeMag
|
||||
|| 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
|
||||
* transient container reloading, temporary GE duplication, or pricing glitches).
|
||||
*/
|
||||
public static List<Snapshot> sanitizeHistory(List<Snapshot> snapshots)
|
||||
{
|
||||
if (snapshots == null || snapshots.size() < 3)
|
||||
{
|
||||
return snapshots == null ? new ArrayList<>() : new ArrayList<>(snapshots);
|
||||
}
|
||||
|
||||
List<Snapshot> list = new ArrayList<>(snapshots);
|
||||
boolean changed = true;
|
||||
int passes = 0;
|
||||
while (changed && passes < 5)
|
||||
{
|
||||
changed = false;
|
||||
passes++;
|
||||
|
||||
// Pass 1: check single-point spikes
|
||||
for (int i = 1; i < list.size() - 1; i++)
|
||||
{
|
||||
Snapshot before = list.get(i - 1);
|
||||
Snapshot cur = list.get(i);
|
||||
Snapshot after = list.get(i + 1);
|
||||
|
||||
if (isTransientSpike(before.t, before.total(), cur.t, cur.total(), after.t, after.total()))
|
||||
{
|
||||
list.remove(i);
|
||||
changed = true;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: check two-point spikes
|
||||
for (int i = 1; i < list.size() - 2; i++)
|
||||
{
|
||||
Snapshot before = list.get(i - 1);
|
||||
Snapshot s1 = list.get(i);
|
||||
Snapshot s2 = list.get(i + 1);
|
||||
Snapshot after = list.get(i + 2);
|
||||
|
||||
if (isDoubleTransientSpike(before.t, before.total(), s1.t, s1.total(), s2.t, s2.total(), after.t, after.total()))
|
||||
{
|
||||
list.remove(i + 1);
|
||||
list.remove(i);
|
||||
changed = true;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: check three-point spikes
|
||||
for (int i = 1; i < list.size() - 3; i++)
|
||||
{
|
||||
Snapshot before = list.get(i - 1);
|
||||
Snapshot s1 = list.get(i);
|
||||
Snapshot s2 = list.get(i + 1);
|
||||
Snapshot s3 = list.get(i + 2);
|
||||
Snapshot after = list.get(i + 3);
|
||||
|
||||
if (isTripleTransientSpike(before.t, before.total(), s1.t, s1.total(), s2.t, s2.total(), s3.t, s3.total(), after.t, after.total()))
|
||||
{
|
||||
list.remove(i + 2);
|
||||
list.remove(i + 1);
|
||||
list.remove(i);
|
||||
changed = true;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static final long FOUR_HOURS_MS = 4 * 3600 * 1000L;
|
||||
|
||||
/**
|
||||
* Records a purchase of an item, managing the 4-hour window rollover.
|
||||
*/
|
||||
public void recordBuy(int itemId, int quantity, long now)
|
||||
{
|
||||
if (quantity <= 0 || itemId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (geBuyLimits == null)
|
||||
{
|
||||
geBuyLimits = new ConcurrentHashMap<>();
|
||||
}
|
||||
GeBuyLimit limit = geBuyLimits.computeIfAbsent(itemId, id -> new GeBuyLimit(id, 0, 0, 0));
|
||||
synchronized (limit)
|
||||
{
|
||||
if (limit.windowStart == 0 || now - limit.windowStart >= FOUR_HOURS_MS)
|
||||
{
|
||||
limit.windowStart = now;
|
||||
limit.bought = quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
limit.bought += quantity;
|
||||
}
|
||||
limit.lastTradeTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the current 4-hour buy limit status for an item.
|
||||
*/
|
||||
public GeLimitInfo getBuyLimitInfo(int itemId, int itemGeLimit, long now)
|
||||
{
|
||||
GeBuyLimit limit = geBuyLimits != null ? geBuyLimits.get(itemId) : null;
|
||||
if (limit == null || limit.windowStart == 0)
|
||||
{
|
||||
return new GeLimitInfo(itemId, itemGeLimit, 0, itemGeLimit, 0, false);
|
||||
}
|
||||
synchronized (limit)
|
||||
{
|
||||
long elapsed = now - limit.windowStart;
|
||||
if (elapsed >= FOUR_HOURS_MS)
|
||||
{
|
||||
// Expired: full limit restored
|
||||
return new GeLimitInfo(itemId, itemGeLimit, 0, itemGeLimit, 0, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
int bought = limit.bought;
|
||||
int remaining = Math.max(0, itemGeLimit > 0 ? itemGeLimit - bought : 0);
|
||||
long resetInMs = FOUR_HOURS_MS - elapsed;
|
||||
return new GeLimitInfo(itemId, itemGeLimit, bought, remaining, resetInMs, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all tracked 4-hour buy limits and slot tracking for this character.
|
||||
*/
|
||||
public void clearBuyLimits()
|
||||
{
|
||||
if (geBuyLimits != null)
|
||||
{
|
||||
geBuyLimits.clear();
|
||||
}
|
||||
geBuyLimitSlots = new GeBuyLimitSlot[8];
|
||||
}
|
||||
|
||||
public void resetHourly(boolean isSession)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
if (isSession)
|
||||
{
|
||||
resetHourlySessionAt = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
resetHourlyNowAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
/**
|
||||
* Tracks purchases of a specific item within the rolling 4-hour Grand Exchange buy limit window.
|
||||
*/
|
||||
public class GeBuyLimit
|
||||
{
|
||||
public int itemId;
|
||||
public int bought;
|
||||
public long windowStart;
|
||||
public long lastTradeTime;
|
||||
|
||||
public GeBuyLimit()
|
||||
{
|
||||
}
|
||||
|
||||
public GeBuyLimit(int itemId, int bought, long windowStart, long lastTradeTime)
|
||||
{
|
||||
this.itemId = itemId;
|
||||
this.bought = bought;
|
||||
this.windowStart = windowStart;
|
||||
this.lastTradeTime = lastTradeTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
/**
|
||||
* Calculated status of an item's 4-hour Grand Exchange buy limit.
|
||||
*/
|
||||
public class GeLimitInfo
|
||||
{
|
||||
public final int itemId;
|
||||
public final int limit;
|
||||
public final int bought;
|
||||
public final int remaining;
|
||||
public final long resetInMs;
|
||||
public final boolean windowActive;
|
||||
|
||||
public GeLimitInfo(int itemId, int limit, int bought, int remaining, long resetInMs, boolean windowActive)
|
||||
{
|
||||
this.itemId = itemId;
|
||||
this.limit = limit;
|
||||
this.bought = bought;
|
||||
this.remaining = remaining;
|
||||
this.resetInMs = resetInMs;
|
||||
this.windowActive = windowActive;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
/** One Grand Exchange slot prepared for display. */
|
||||
public class GeRow
|
||||
{
|
||||
public final String character;
|
||||
public final int slot;
|
||||
/** null = empty slot */
|
||||
public final GeSlot offer;
|
||||
public final String name;
|
||||
public final int marketPrice;
|
||||
public final GeLimitInfo limitInfo;
|
||||
|
||||
public GeRow(String character, int slot, GeSlot offer, String name, int marketPrice)
|
||||
{
|
||||
this(character, slot, offer, name, marketPrice, null);
|
||||
}
|
||||
|
||||
public GeRow(String character, int slot, GeSlot offer, String name, int marketPrice, GeLimitInfo limitInfo)
|
||||
{
|
||||
this.character = character;
|
||||
this.slot = slot;
|
||||
this.offer = offer;
|
||||
this.name = name;
|
||||
this.marketPrice = marketPrice;
|
||||
this.limitInfo = limitInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
/** Last known state of one Grand Exchange slot, persisted so collections can be told apart from gains. */
|
||||
public class GeSlot
|
||||
{
|
||||
public int item;
|
||||
public boolean buy;
|
||||
public long total;
|
||||
public long price;
|
||||
|
||||
/** items / coins already taken out of the collection box (fallback tracking) */
|
||||
public long collectedItems;
|
||||
public long collectedCoins;
|
||||
|
||||
/** value held by the slot at the last update */
|
||||
public long escrowItems;
|
||||
public long escrowCoins;
|
||||
public long collectItems;
|
||||
public long collectCoins;
|
||||
|
||||
/** live offer details */
|
||||
public long done;
|
||||
public long spent;
|
||||
public String state;
|
||||
/** when the offer was placed (0 = before tracking started, see trackedSince) */
|
||||
public long placedAt;
|
||||
public long trackedSince;
|
||||
public long trackedDone;
|
||||
public long lastTradeAt;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/** Item holdings over time, stored separately from the wealth timeline (<key>.items.json). */
|
||||
public class ItemLedger
|
||||
{
|
||||
public List<ItemSnapshot> snapshots = new CopyOnWriteArrayList<>();
|
||||
|
||||
public transient long fileModified;
|
||||
public transient boolean dirty;
|
||||
|
||||
public ItemSnapshot last()
|
||||
{
|
||||
List<ItemSnapshot> s = snapshots;
|
||||
return s.isEmpty() ? null : s.get(s.size() - 1);
|
||||
}
|
||||
|
||||
/** Latest snapshot taken at or before {@code t}, or the first one after it if none exists. */
|
||||
public ItemSnapshot at(long t)
|
||||
{
|
||||
List<ItemSnapshot> s = snapshots;
|
||||
ItemSnapshot best = null;
|
||||
for (ItemSnapshot x : s)
|
||||
{
|
||||
if (x.t <= t)
|
||||
{
|
||||
best = x;
|
||||
}
|
||||
else
|
||||
{
|
||||
return best != null ? best : x;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
public void normalize()
|
||||
{
|
||||
List<ItemSnapshot> s = snapshots == null ? new ArrayList<>() : new ArrayList<>(snapshots);
|
||||
s.removeIf(x -> x == null || x.ids == null || x.q == null || x.p == null
|
||||
|| x.ids.length != x.q.length || x.ids.length != x.p.length);
|
||||
s.sort((a, b) -> Long.compare(a.t, b.t));
|
||||
snapshots = new CopyOnWriteArrayList<>(s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
/** One priced item stack for the item viewer. */
|
||||
public class ItemRow
|
||||
{
|
||||
public final int id;
|
||||
public final String name;
|
||||
public final long qty;
|
||||
public final long price;
|
||||
public final long value;
|
||||
|
||||
public ItemRow(int id, String name, long qty, long price)
|
||||
{
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.qty = qty;
|
||||
this.price = price;
|
||||
this.value = qty * price;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Holdings of a character at one moment: canonical item id, quantity and the GE price at that time.
|
||||
* Allows splitting wealth changes into "market" (price) and "activity" (quantity) effects.
|
||||
*/
|
||||
public class ItemSnapshot
|
||||
{
|
||||
public long t;
|
||||
public int[] ids;
|
||||
public long[] q;
|
||||
public int[] p;
|
||||
/** pinned (session start) - kept by compaction */
|
||||
public Boolean k;
|
||||
|
||||
public ItemSnapshot()
|
||||
{
|
||||
}
|
||||
|
||||
public ItemSnapshot(long t, int[] ids, long[] q, int[] p)
|
||||
{
|
||||
this.t = t;
|
||||
this.ids = ids;
|
||||
this.q = q;
|
||||
this.p = p;
|
||||
}
|
||||
|
||||
public boolean sameAs(ItemSnapshot other)
|
||||
{
|
||||
return other != null && Arrays.equals(ids, other.ids) && Arrays.equals(q, other.q) && Arrays.equals(p, other.p);
|
||||
}
|
||||
|
||||
public boolean sameQuantities(ItemSnapshot other)
|
||||
{
|
||||
return other != null && Arrays.equals(ids, other.ids) && Arrays.equals(q, other.q);
|
||||
}
|
||||
|
||||
public int indexOf(int id)
|
||||
{
|
||||
return ids == null ? -1 : Arrays.binarySearch(ids, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
import com.example.essentialsplus.WealthCategory;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* One point on the wealth timeline.
|
||||
* <p>
|
||||
* Raw points only have {@code t} and {@code v}. Aggregated points (hourly / daily buckets of older data)
|
||||
* additionally hold open, close, low and high of the total and store the time-weighted average
|
||||
* breakdown in {@code v}.
|
||||
*/
|
||||
public class Snapshot
|
||||
{
|
||||
public static final String HOUR = "h";
|
||||
public static final String DAY = "d";
|
||||
|
||||
public long t;
|
||||
public long[] v;
|
||||
/** open / close / low / high total - only set on aggregates */
|
||||
public Long o;
|
||||
public Long c;
|
||||
public Long lo;
|
||||
public Long hi;
|
||||
/** granularity of an aggregate: "h" or "d" */
|
||||
public String g;
|
||||
/** cumulative value change from GE price movements up to this point ({@link AccountData#market}); null on old data */
|
||||
public Long m;
|
||||
|
||||
public Snapshot()
|
||||
{
|
||||
}
|
||||
|
||||
public Snapshot(long t, long[] v)
|
||||
{
|
||||
this.t = t;
|
||||
this.v = v;
|
||||
}
|
||||
|
||||
public long total()
|
||||
{
|
||||
long sum = 0;
|
||||
if (v != null)
|
||||
{
|
||||
for (long x : v)
|
||||
{
|
||||
sum += x;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
public long get(WealthCategory category)
|
||||
{
|
||||
int i = category.ordinal();
|
||||
return v != null && i < v.length ? v[i] : 0;
|
||||
}
|
||||
|
||||
public long low()
|
||||
{
|
||||
return lo != null ? lo : total();
|
||||
}
|
||||
|
||||
public long high()
|
||||
{
|
||||
return hi != null ? hi : total();
|
||||
}
|
||||
|
||||
public long open()
|
||||
{
|
||||
return o != null ? o : total();
|
||||
}
|
||||
|
||||
public long close()
|
||||
{
|
||||
return c != null ? c : total();
|
||||
}
|
||||
|
||||
public boolean isAggregate()
|
||||
{
|
||||
return g != null || lo != null;
|
||||
}
|
||||
|
||||
/** Pads arrays loaded from older files to the current category count. */
|
||||
public void normalize()
|
||||
{
|
||||
if (v == null)
|
||||
{
|
||||
v = new long[WealthCategory.COUNT];
|
||||
}
|
||||
else if (v.length < WealthCategory.COUNT)
|
||||
{
|
||||
v = Arrays.copyOf(v, WealthCategory.COUNT);
|
||||
}
|
||||
if (lo != null && g == null)
|
||||
{
|
||||
g = DAY;
|
||||
}
|
||||
}
|
||||
|
||||
public long market()
|
||||
{
|
||||
return m != null ? m : 0;
|
||||
}
|
||||
|
||||
/** Copy with {@code delta} removed from the total, spread over the categories by their share. */
|
||||
public Snapshot shifted(long delta)
|
||||
{
|
||||
long[] nv = v == null ? new long[0] : v.clone();
|
||||
if (delta != 0 && nv.length > 0)
|
||||
{
|
||||
long base = 0;
|
||||
for (long x : nv)
|
||||
{
|
||||
base += Math.abs(x);
|
||||
}
|
||||
long left = delta;
|
||||
int largest = 0;
|
||||
for (int i = 0; i < nv.length; i++)
|
||||
{
|
||||
if (Math.abs(nv[i]) > Math.abs(nv[largest]))
|
||||
{
|
||||
largest = i;
|
||||
}
|
||||
if (base > 0)
|
||||
{
|
||||
long part = Math.round((double) delta * Math.abs(nv[i]) / base);
|
||||
nv[i] -= part;
|
||||
left -= part;
|
||||
}
|
||||
}
|
||||
nv[largest] -= left;
|
||||
}
|
||||
Snapshot s = new Snapshot(t, nv);
|
||||
s.g = g;
|
||||
s.m = m;
|
||||
s.o = o == null ? null : o - delta;
|
||||
s.c = c == null ? null : c - delta;
|
||||
s.lo = lo == null ? null : lo - delta;
|
||||
s.hi = hi == null ? null : hi - delta;
|
||||
return s;
|
||||
}
|
||||
|
||||
public boolean sameValues(long[] other)
|
||||
{
|
||||
return Arrays.equals(v, other);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
/** Value of a single wealth source (e.g. "Looting bag") inside a category. */
|
||||
public class SourceValue
|
||||
{
|
||||
public String name;
|
||||
public int category;
|
||||
public long value;
|
||||
|
||||
public SourceValue()
|
||||
{
|
||||
}
|
||||
|
||||
public SourceValue(String name, int category, long value)
|
||||
{
|
||||
this.name = name;
|
||||
this.category = category;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.essentialsplus.data;
|
||||
|
||||
public class TopItem
|
||||
{
|
||||
public int id;
|
||||
public String name;
|
||||
public long qty;
|
||||
public long value;
|
||||
|
||||
public TopItem()
|
||||
{
|
||||
}
|
||||
|
||||
public TopItem(int id, String name, long qty, long value)
|
||||
{
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.qty = qty;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user