feat: player tracker
This commit is contained in:
@@ -49,16 +49,16 @@ public class AccountData
|
||||
public static class GeBuyLimitSlot
|
||||
{
|
||||
public int itemId;
|
||||
public int price;
|
||||
public long price;
|
||||
public int total;
|
||||
public int quantity;
|
||||
public int spent;
|
||||
public long spent;
|
||||
|
||||
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.price = price;
|
||||
@@ -302,6 +302,82 @@ public class AccountData
|
||||
|| 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
|
||||
* transient container reloading, temporary GE duplication, or pricing glitches).
|
||||
@@ -371,10 +447,88 @@ public class AccountData
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
@@ -459,4 +613,3 @@ public class AccountData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ public class GeRow
|
||||
/** null = empty slot */
|
||||
public final GeSlot offer;
|
||||
public final String name;
|
||||
public final int marketPrice;
|
||||
public final long marketPrice;
|
||||
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);
|
||||
}
|
||||
|
||||
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.slot = slot;
|
||||
|
||||
@@ -27,4 +27,9 @@ public class GeSlot
|
||||
public long trackedSince;
|
||||
public long trackedDone;
|
||||
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 int[] ids;
|
||||
public long[] q;
|
||||
public int[] p;
|
||||
public long[] p;
|
||||
/** pinned (session start) - kept by compaction */
|
||||
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.ids = ids;
|
||||
|
||||
Reference in New Issue
Block a user