dankgrinder 6.37.0 → 6.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/beg.js +26 -4
- package/lib/commands/farm.js +36 -10
- package/lib/commands/fishVision.js +27 -1
- package/lib/commands/index.js +2 -3
- package/lib/commands/profile.js +200 -52
- package/lib/commands/stream.js +1 -1
- package/lib/commands/utils.js +4 -2
- package/lib/commands/work.js +1 -1
- package/lib/grinder.js +390 -142
- package/lib/rawLogger.js +13 -11
- package/lib/structures.js +19 -26
- package/package.json +1 -1
- package/lib/commands/scratch.js +0 -83
package/lib/grinder.js
CHANGED
|
@@ -5,7 +5,7 @@ const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/
|
|
|
5
5
|
const rawLogger = require('./rawLogger');
|
|
6
6
|
const {
|
|
7
7
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
8
|
-
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue,
|
|
8
|
+
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue,
|
|
9
9
|
} = require('./structures');
|
|
10
10
|
const PKG_VERSION = require('../package.json').version;
|
|
11
11
|
|
|
@@ -94,6 +94,8 @@ const c = {
|
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
const WORKER_COLORS = [c.cyan, c.magenta, c.yellow, c.green, c.blue, c.red];
|
|
97
|
+
// Unique marker written to stdout so we can query cursor position via DSR response
|
|
98
|
+
const MARKER = '\x1b[6n\x1b[@@MARKER@@';
|
|
97
99
|
const DANK_MEMER_ID = '270904126974590976';
|
|
98
100
|
|
|
99
101
|
// ── Safe options for search/crime ──────────────────────────
|
|
@@ -277,9 +279,9 @@ function boxTop(w, color) { return color + BOX.dtl + BOX.dh.repeat(w - 2) + BOX.
|
|
|
277
279
|
function boxMid(w, color) { return color + BOX.tee + BOX.h.repeat(w - 2) + BOX.teeR + c.reset; }
|
|
278
280
|
function boxBot(w, color) { return color + BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr + c.reset; }
|
|
279
281
|
function boxLine(content, w, color) {
|
|
280
|
-
const stripped = content
|
|
282
|
+
const stripped = stripAnsi(content);
|
|
281
283
|
const pad = Math.max(0, w - 4 - stripped.length);
|
|
282
|
-
return color + BOX.dv + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
|
|
284
|
+
return color + BOX.dv + c.reset + ' ' + stripAnsi(content) + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
|
|
283
285
|
}
|
|
284
286
|
function thinLine(w) { return ' ' + c.dim + BOX.h.repeat(w - 4) + c.reset; }
|
|
285
287
|
|
|
@@ -866,8 +868,8 @@ const CMD_NAMES_CLEAN = {
|
|
|
866
868
|
bj: 'Blackjack', blackjack: 'Blackjack', hl: 'High Low', pm: 'Post Memes', postmemes: 'Post Memes',
|
|
867
869
|
ct: 'Coin Toss', cointoss: 'Coin Toss', se: 'Snake Eyes', snakeeyes: 'Snake Eyes',
|
|
868
870
|
hunt: 'Hunt', dig: 'Dig', fish: 'Fish', beg: 'Beg', search: 'Search', crime: 'Crime',
|
|
869
|
-
tidy: 'Tidy', farm: 'Farm', daily: 'Daily',
|
|
870
|
-
|
|
871
|
+
tidy: 'Tidy', farm: 'Farm', daily: 'Daily', monthly: 'Monthly',
|
|
872
|
+
adventure: 'Adventure', trivia: 'Trivia', stream: 'Stream',
|
|
871
873
|
drops: 'Drops', use: 'Use Item', dep: 'Deposit', deposit: 'Deposit', inv: 'Inventory',
|
|
872
874
|
work: 'Work', stream: 'Stream', roulette: 'Roulette', slots: 'Slots',
|
|
873
875
|
};
|
|
@@ -1110,6 +1112,11 @@ class AccountWorker {
|
|
|
1110
1112
|
this.commandQueue = null;
|
|
1111
1113
|
this.lastHealthCheck = Date.now();
|
|
1112
1114
|
this.doneToday = new Map();
|
|
1115
|
+
|
|
1116
|
+
// Dynamic cooldown learning: tracks actual parsed cooldowns per command.
|
|
1117
|
+
// Persisted in Redis hash `dkg:cd:learned:{accountId}` for cross-restart persistence.
|
|
1118
|
+
// Used as adaptive floor when command parsers don't return exact cooldowns.
|
|
1119
|
+
this._learnedCooldowns = new Map();
|
|
1113
1120
|
this._fishRoundsSinceSell = 0;
|
|
1114
1121
|
this._autoDepositThreshold = account.auto_deposit_threshold || 500000;
|
|
1115
1122
|
|
|
@@ -1483,7 +1490,9 @@ class AccountWorker {
|
|
|
1483
1490
|
totalValue: result.totalValue || 0,
|
|
1484
1491
|
}),
|
|
1485
1492
|
});
|
|
1486
|
-
} catch {
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
console.error(`[${this.username}] inventory API error:`, err?.message || err);
|
|
1495
|
+
}
|
|
1487
1496
|
|
|
1488
1497
|
return {
|
|
1489
1498
|
ok: true,
|
|
@@ -1517,6 +1526,60 @@ class AccountWorker {
|
|
|
1517
1526
|
}
|
|
1518
1527
|
}
|
|
1519
1528
|
|
|
1529
|
+
// ── Profile Check ────────────────────────────────────────────
|
|
1530
|
+
async checkProfile(silent = false) {
|
|
1531
|
+
try {
|
|
1532
|
+
const result = await commands.runProfile({
|
|
1533
|
+
channel: this.channel,
|
|
1534
|
+
waitForDankMemer: (t) => this.waitForDankMemer(t),
|
|
1535
|
+
accountId: this.account.id,
|
|
1536
|
+
redis,
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
if (result.error) {
|
|
1540
|
+
if (!silent) this.log('warn', `[profile] Check failed: ${result.error}`);
|
|
1541
|
+
return result;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (!silent) {
|
|
1545
|
+
this.log('info', `[profile] Level ${result.level} · ⏣ ${(result.wallet || 0).toLocaleString()} wallet · ⏣ ${(result.net || 0).toLocaleString()} net`);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Push to web dashboard via grinder status endpoint
|
|
1549
|
+
try {
|
|
1550
|
+
await fetch(`${API_URL}/api/grinder/profile`, {
|
|
1551
|
+
method: 'POST',
|
|
1552
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
1553
|
+
body: JSON.stringify({
|
|
1554
|
+
account_id: this.account.id,
|
|
1555
|
+
level: result.level,
|
|
1556
|
+
xp_current: result.xpCurrent,
|
|
1557
|
+
xp_max: result.xpMax,
|
|
1558
|
+
wallet: result.wallet,
|
|
1559
|
+
bank: result.bank,
|
|
1560
|
+
net: result.net,
|
|
1561
|
+
items_unique: result.itemsUnique,
|
|
1562
|
+
items_total: result.itemsTotal,
|
|
1563
|
+
items_worth: result.itemsWorth,
|
|
1564
|
+
cmds_total: result.cmdsTotal,
|
|
1565
|
+
favorite: result.favorite,
|
|
1566
|
+
badges: result.badges || [],
|
|
1567
|
+
pets: result.pets || [],
|
|
1568
|
+
showcase: result.showcase || [],
|
|
1569
|
+
raw_text: result.rawText || '',
|
|
1570
|
+
}),
|
|
1571
|
+
});
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
console.error(`[${this.username}] profile API error:`, err?.message || err);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
return { ok: true, ...result };
|
|
1577
|
+
} catch (e) {
|
|
1578
|
+
if (!silent) this.log('warn', `[profile] Check error: ${e.message}`);
|
|
1579
|
+
return { ok: false, error: e.message };
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1520
1583
|
async checkBalance(silent = false) {
|
|
1521
1584
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
1522
1585
|
const sentAt = Date.now();
|
|
@@ -1717,22 +1780,48 @@ class AccountWorker {
|
|
|
1717
1780
|
// Each modular command handler sends the command, waits for response,
|
|
1718
1781
|
// handles Hold Tight / cooldowns / item-buying internally.
|
|
1719
1782
|
async runCommand(cmdName, prefix) {
|
|
1720
|
-
|
|
1783
|
+
// Slash commands use '/cmd' without a space between slash and subcommand
|
|
1784
|
+
// (e.g. '/dep max', '/work shift'). Legacy prefix uses 'pls cmd'.
|
|
1785
|
+
const SLASH_CMD = new Map([
|
|
1786
|
+
['dep max', '/dep max'],
|
|
1787
|
+
['with max', '/with max'],
|
|
1788
|
+
['work shift', '/work shift'],
|
|
1789
|
+
['monthly', '/monthly'],
|
|
1790
|
+
]);
|
|
1721
1791
|
const bjBet = Math.max(5000, this.account.bet_amount || 5000);
|
|
1722
1792
|
const gambBet = Math.max(10000, this.account.bet_amount || 10000);
|
|
1723
1793
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1794
|
+
let cmdString;
|
|
1795
|
+
if (prefix === '/') {
|
|
1796
|
+
const slashVariant = SLASH_CMD.get(cmdName);
|
|
1797
|
+
if (slashVariant) {
|
|
1798
|
+
cmdString = slashVariant;
|
|
1799
|
+
} else if (cmdName === 'blackjack') {
|
|
1800
|
+
cmdString = `/bj ${bjBet}`;
|
|
1801
|
+
} else if (cmdName === 'cointoss') {
|
|
1802
|
+
cmdString = `/cointoss ${gambBet}`;
|
|
1803
|
+
} else if (cmdName === 'roulette') {
|
|
1804
|
+
cmdString = `/roulette ${gambBet}`;
|
|
1805
|
+
} else if (cmdName === 'slots') {
|
|
1806
|
+
cmdString = `/slots ${gambBet}`;
|
|
1807
|
+
} else if (cmdName === 'snakeeyes') {
|
|
1808
|
+
cmdString = `/snakeeyes ${gambBet}`;
|
|
1809
|
+
} else {
|
|
1810
|
+
cmdString = `/${cmdName}`;
|
|
1811
|
+
}
|
|
1812
|
+
} else {
|
|
1813
|
+
switch (cmdName) {
|
|
1814
|
+
case 'dep max': cmdString = `${prefix} dep max`; break;
|
|
1815
|
+
case 'with max': cmdString = `${prefix} with max`; break;
|
|
1816
|
+
case 'blackjack': cmdString = `${prefix} bj ${bjBet}`; break;
|
|
1817
|
+
case 'cointoss': cmdString = `${prefix} cointoss ${gambBet}`; break;
|
|
1818
|
+
case 'roulette': cmdString = `${prefix} roulette ${gambBet}`; break;
|
|
1819
|
+
case 'slots': cmdString = `${prefix} slots ${gambBet}`; break;
|
|
1820
|
+
case 'snakeeyes': cmdString = `${prefix} snakeeyes ${gambBet}`; break;
|
|
1821
|
+
case 'work shift': cmdString = `${prefix} work shift`; break;
|
|
1822
|
+
case 'monthly': cmdString = `${prefix} monthly`; break;
|
|
1823
|
+
default: cmdString = `${prefix} ${cmdName}`;
|
|
1824
|
+
}
|
|
1736
1825
|
}
|
|
1737
1826
|
|
|
1738
1827
|
if (shutdownCalled || !this.running) return;
|
|
@@ -1830,7 +1919,6 @@ class AccountWorker {
|
|
|
1830
1919
|
case 'hunt': cmdResult = await commands.runHunt(cmdOpts); break;
|
|
1831
1920
|
case 'dig': cmdResult = await commands.runDig(cmdOpts); break;
|
|
1832
1921
|
case 'fish': cmdResult = await commands.runFish(cmdOpts); break;
|
|
1833
|
-
case 'scratch': cmdResult = await commands.runScratch(cmdOpts); break;
|
|
1834
1922
|
case 'adventure': cmdResult = await commands.runAdventure(cmdOpts); break;
|
|
1835
1923
|
case 'blackjack': cmdResult = await commands.runBlackjack(cmdOpts); break;
|
|
1836
1924
|
case 'trivia': cmdResult = await commands.runTrivia(cmdOpts); break;
|
|
@@ -1885,7 +1973,9 @@ class AccountWorker {
|
|
|
1885
1973
|
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
1886
1974
|
body: JSON.stringify({ account_id: this.account.id, active: false }),
|
|
1887
1975
|
});
|
|
1888
|
-
} catch {
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
console.error(`[${this.username}] status deactivation API error:`, err?.message || err);
|
|
1978
|
+
}
|
|
1889
1979
|
await sendLog(this.username, cmdString, 'VERIFICATION — account deactivated', 'error');
|
|
1890
1980
|
sendWebhook('CAPTCHA ALERT', `**${this.username}** needs verification!\nCommand: \`${cmdName}\`\nSolve in Discord and re-enable from dashboard.`, 0xef4444);
|
|
1891
1981
|
return;
|
|
@@ -1964,8 +2054,9 @@ class AccountWorker {
|
|
|
1964
2054
|
const timeMatch = result.match(/<t:(\d+):R>/);
|
|
1965
2055
|
let waitSec;
|
|
1966
2056
|
if (timeMatch) {
|
|
1967
|
-
|
|
1968
|
-
|
|
2057
|
+
// Discord <t:TS:R> format: :R = relative seconds from NOW (not Unix ms).
|
|
2058
|
+
// The captured number IS already the number of seconds to wait.
|
|
2059
|
+
waitSec = Math.max(60, parseInt(timeMatch[1]));
|
|
1969
2060
|
} else {
|
|
1970
2061
|
const defaultWaits = { daily: 86400, weekly: 604800, monthly: 2592000 };
|
|
1971
2062
|
waitSec = defaultWaits[cmdName] || 86400;
|
|
@@ -1995,6 +2086,11 @@ class AccountWorker {
|
|
|
1995
2086
|
if (cmdResult.nextCooldownSec) {
|
|
1996
2087
|
await this.setCooldown(cmdName, cmdResult.nextCooldownSec);
|
|
1997
2088
|
this._lastCooldownOverride = cmdResult.nextCooldownSec;
|
|
2089
|
+
// Learn: record this cooldown as the known value for future fallback use
|
|
2090
|
+
this._learnedCooldowns.set(cmdName, cmdResult.nextCooldownSec);
|
|
2091
|
+
if (redis) {
|
|
2092
|
+
await redis.hset(`dkg:cd:learned:${this.account.id}`, cmdName, String(cmdResult.nextCooldownSec));
|
|
2093
|
+
}
|
|
1998
2094
|
}
|
|
1999
2095
|
|
|
2000
2096
|
// Smart gambling loss tracker
|
|
@@ -2115,19 +2211,38 @@ class AccountWorker {
|
|
|
2115
2211
|
}
|
|
2116
2212
|
}
|
|
2117
2213
|
|
|
2214
|
+
// Load previously learned cooldowns from Redis so floors adapt across restarts.
|
|
2215
|
+
async _loadLearnedCooldowns() {
|
|
2216
|
+
if (!redis) return;
|
|
2217
|
+
try {
|
|
2218
|
+
const learned = await redis.hgetall(`dkg:cd:learned:${this.account.id}`);
|
|
2219
|
+
for (const [cmd, val] of Object.entries(learned)) {
|
|
2220
|
+
const n = parseFloat(val);
|
|
2221
|
+
if (Number.isFinite(n) && n > 0) {
|
|
2222
|
+
this._learnedCooldowns.set(cmd, n);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
if (this._learnedCooldowns.size > 0) {
|
|
2226
|
+
this.log('info', `Loaded ${this._learnedCooldowns.size} learned cooldowns from Redis`);
|
|
2227
|
+
}
|
|
2228
|
+
} catch (e) {
|
|
2229
|
+
this.log('warn', `Failed to load learned cooldowns: ${e.message}`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2118
2233
|
printStats() {
|
|
2119
2234
|
// Stats are shown in the live dashboard, no-op here
|
|
2120
2235
|
}
|
|
2121
2236
|
|
|
2122
2237
|
// ── Command Map (shared across ticks, used to build the heap) ──
|
|
2123
2238
|
// Priority: higher = runs first when multiple commands are ready simultaneously.
|
|
2124
|
-
// 10 = time-gated (daily/
|
|
2239
|
+
// 10 = time-gated (daily/monthly — never miss),
|
|
2125
2240
|
// 8 = financial safety (deposit),
|
|
2126
2241
|
// 7 = gambling fast-cycle (2-3s CD — run MOST often),
|
|
2127
2242
|
// 6 = fast grinders (10s CD),
|
|
2128
2243
|
// 5 = medium grinders (20-40s CD),
|
|
2129
2244
|
// 4 = resource grinders (hunt/dig — need items),
|
|
2130
|
-
// 3 = interactive/long CD (adventure/stream/work
|
|
2245
|
+
// 3 = interactive/long CD (adventure/stream/work),
|
|
2131
2246
|
// 2 = utility (drops/use/tidy)
|
|
2132
2247
|
static COMMAND_MAP = [
|
|
2133
2248
|
// Gambling — 2-3s CD, highest frequency
|
|
@@ -2138,7 +2253,7 @@ class AccountWorker {
|
|
|
2138
2253
|
{ key: 'cmd_snakeeyes', cmd: 'snakeeyes', cdKey: 'cd_snakeeyes', defaultCd: 3, priority: 7 },
|
|
2139
2254
|
// Fast grinders — 10s CD
|
|
2140
2255
|
{ key: 'cmd_hl', cmd: 'hl', cdKey: 'cd_hl', defaultCd: 10, priority: 6 },
|
|
2141
|
-
|
|
2256
|
+
{ key: 'cmd_farm', cmd: 'farm', cdKey: 'cd_farm', defaultCd: 30, priority: 4 },
|
|
2142
2257
|
{ key: 'cmd_trivia', cmd: 'trivia', cdKey: 'cd_trivia', defaultCd: 10, priority: 6 },
|
|
2143
2258
|
{ key: 'cmd_use', cmd: 'use', cdKey: 'cd_use', defaultCd: 10, priority: 2 },
|
|
2144
2259
|
// Medium grinders — 20-25s CD
|
|
@@ -2149,24 +2264,23 @@ class AccountWorker {
|
|
|
2149
2264
|
{ key: 'cmd_search', cmd: 'search', cdKey: 'cd_search', defaultCd: 25, priority: 5 },
|
|
2150
2265
|
// Slow grinders — 40s CD
|
|
2151
2266
|
{ key: 'cmd_beg', cmd: 'beg', cdKey: 'cd_beg', defaultCd: 40, priority: 5 },
|
|
2152
|
-
{ key: 'cmd_crime', cmd: 'crime',
|
|
2153
|
-
{ key: 'cmd_tidy', cmd: 'tidy',
|
|
2267
|
+
{ key: 'cmd_crime', cmd: 'crime', cdKey: 'cd_crime', defaultCd: 40, priority: 5 },
|
|
2268
|
+
{ key: 'cmd_tidy', cmd: 'tidy', cdKey: 'cd_tidy', defaultCd: 40, priority: 2 },
|
|
2154
2269
|
// Interactive — response-driven CD (handler sets nextCooldownSec)
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
2270
|
+
{ key: 'cmd_adventure', cmd: 'adventure', cdKey: 'cd_adventure', defaultCd: 300, priority: 3 },
|
|
2271
|
+
{ key: 'cmd_stream', cmd: 'stream', cdKey: 'cd_stream', defaultCd: 600, priority: 3 },
|
|
2272
|
+
{ key: 'cmd_work', cmd: 'work shift', cdKey: 'cd_work', defaultCd: 1800, priority: 3 },
|
|
2159
2273
|
// Time-gated (run ASAP when available)
|
|
2160
|
-
{ key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily',
|
|
2161
|
-
//
|
|
2162
|
-
{ key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly',
|
|
2274
|
+
{ key: 'cmd_daily', cmd: 'daily', cdKey: 'cd_daily', defaultCd: 86400, priority: 10 },
|
|
2275
|
+
// monthly — premium only
|
|
2276
|
+
{ key: 'cmd_monthly', cmd: 'monthly', cdKey: 'cd_monthly', defaultCd: 2592000, priority: 10 },
|
|
2163
2277
|
// Financial safety
|
|
2164
|
-
{ key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit',
|
|
2165
|
-
{ key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops',
|
|
2278
|
+
{ key: 'cmd_deposit', cmd: 'dep max', cdKey: 'cd_deposit', defaultCd: 3600, priority: 8 },
|
|
2279
|
+
{ key: 'cmd_drops', cmd: 'drops', cdKey: 'cd_drops', defaultCd: 86400, priority: 2 },
|
|
2166
2280
|
// Alert is NOT scheduled — it's reactive (listener-based, see grindLoop)
|
|
2167
2281
|
].map(Object.freeze);
|
|
2168
2282
|
|
|
2169
|
-
buildCommandQueue() {
|
|
2283
|
+
async buildCommandQueue() {
|
|
2170
2284
|
const heap = new MinHeap();
|
|
2171
2285
|
const now = Date.now();
|
|
2172
2286
|
let enabled = AccountWorker.COMMAND_MAP.filter(
|
|
@@ -2180,8 +2294,33 @@ class AccountWorker {
|
|
|
2180
2294
|
enabled = AccountWorker.COMMAND_MAP.filter(ci => Boolean(this.account[ci.key]));
|
|
2181
2295
|
}
|
|
2182
2296
|
|
|
2297
|
+
// Restore cooldown state from Redis so commands don't re-run immediately
|
|
2298
|
+
// after restart. We use TTL to calculate remaining cooldown time.
|
|
2299
|
+
const accountId = this.account.id;
|
|
2300
|
+
const cmdKeys = enabled.map(info => `dkg:cd:${accountId}:${info.cmd}`);
|
|
2301
|
+
let ttlMap = new Map();
|
|
2302
|
+
if (redis) {
|
|
2303
|
+
const pipeline = redis.pipeline();
|
|
2304
|
+
for (const k of cmdKeys) pipeline.ttl(k);
|
|
2305
|
+
const results = await pipeline.exec();
|
|
2306
|
+
for (let i = 0; i < cmdKeys.length; i++) {
|
|
2307
|
+
const [err, val] = results[i];
|
|
2308
|
+
if (!err && Number.isFinite(val) && val > 0) {
|
|
2309
|
+
ttlMap.set(cmdKeys[i], val);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2183
2314
|
for (const info of enabled) {
|
|
2184
|
-
|
|
2315
|
+
let nextRunAt = now;
|
|
2316
|
+
const key = `dkg:cd:${accountId}:${info.cmd}`;
|
|
2317
|
+
const ttlVal = ttlMap.get(key);
|
|
2318
|
+
if (Number.isFinite(ttlVal) && ttlVal > 0) {
|
|
2319
|
+
nextRunAt = now + ttlVal * 1000;
|
|
2320
|
+
this._cooldownBloom.add(key);
|
|
2321
|
+
this.log('info', `Restored cooldown for ${info.cmd}: ${ttlVal}s remaining`);
|
|
2322
|
+
}
|
|
2323
|
+
heap.push({ cmd: info.cmd, nextRunAt, priority: info.priority, info });
|
|
2185
2324
|
}
|
|
2186
2325
|
return heap;
|
|
2187
2326
|
}
|
|
@@ -2309,7 +2448,7 @@ class AccountWorker {
|
|
|
2309
2448
|
|
|
2310
2449
|
// Resume grind loop if it was running
|
|
2311
2450
|
if (!this.commandQueue || this.commandQueue.size === 0) {
|
|
2312
|
-
this.commandQueue = this.buildCommandQueue();
|
|
2451
|
+
this.commandQueue = await this.buildCommandQueue();
|
|
2313
2452
|
}
|
|
2314
2453
|
} else {
|
|
2315
2454
|
this.log('error', 'Recovered connection but channel not found — retrying');
|
|
@@ -2407,98 +2546,65 @@ class AccountWorker {
|
|
|
2407
2546
|
}
|
|
2408
2547
|
|
|
2409
2548
|
if (!this.commandQueue || this.commandQueue.size === 0) {
|
|
2410
|
-
this.commandQueue = this.buildCommandQueue();
|
|
2549
|
+
this.commandQueue = await this.buildCommandQueue();
|
|
2411
2550
|
}
|
|
2412
2551
|
if (!this.commandQueue || this.commandQueue.size === 0) {
|
|
2413
2552
|
this.tickTimeout = setTimeout(() => this.tick(), 15000);
|
|
2414
2553
|
return;
|
|
2415
2554
|
}
|
|
2416
2555
|
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2556
|
+
// ── Scan entire queue for ready commands ─────────────────────
|
|
2557
|
+
// Previously we only peeked the top item, missing other commands that
|
|
2558
|
+
// were already ready while a slower command was at the top of the heap.
|
|
2559
|
+
// Now we drain the queue and separate ready vs. waiting commands.
|
|
2560
|
+
// All ready commands fire immediately — no priority, FIFO order.
|
|
2561
|
+
const readyItems = [];
|
|
2562
|
+
const waitingItems = [];
|
|
2563
|
+
while (this.commandQueue.size > 0) {
|
|
2564
|
+
const item = this.commandQueue.pop();
|
|
2565
|
+
if (!item) break;
|
|
2566
|
+
if (item.nextRunAt <= now) {
|
|
2567
|
+
readyItems.push(item);
|
|
2568
|
+
} else {
|
|
2569
|
+
waitingItems.push(item);
|
|
2570
|
+
}
|
|
2429
2571
|
}
|
|
2430
2572
|
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
item.nextRunAt = now + cd * 1000;
|
|
2435
|
-
if (this.commandQueue) this.commandQueue.push(item);
|
|
2436
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2437
|
-
return;
|
|
2573
|
+
// Re-insert waiting items back into the queue (they still have time to wait)
|
|
2574
|
+
for (const item of waitingItems) {
|
|
2575
|
+
this.commandQueue.push(item);
|
|
2438
2576
|
}
|
|
2439
2577
|
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
item.nextRunAt
|
|
2445
|
-
if (
|
|
2446
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2447
|
-
return;
|
|
2448
|
-
}
|
|
2449
|
-
if (redis) {
|
|
2450
|
-
try {
|
|
2451
|
-
const done = await redis.get(`dkg:done:${this.account.id}:${item.cmd}`);
|
|
2452
|
-
if (done) {
|
|
2453
|
-
const ttlMap = { daily: 86400, weekly: 604800, monthly: 2592000, drops: 86400 };
|
|
2454
|
-
const ttl = ttlMap[item.cmd] || 86400;
|
|
2455
|
-
const expiry = now + ttl * 1000;
|
|
2456
|
-
this.doneToday.set(item.cmd, expiry);
|
|
2457
|
-
item.nextRunAt = expiry;
|
|
2458
|
-
if (this.commandQueue) this.commandQueue.push(item);
|
|
2459
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2460
|
-
return;
|
|
2461
|
-
}
|
|
2462
|
-
} catch {}
|
|
2578
|
+
if (readyItems.length === 0) {
|
|
2579
|
+
// Nothing ready — wait until the soonest command becomes available.
|
|
2580
|
+
let minWaitMs = 5000; // cap at 5s to stay responsive
|
|
2581
|
+
for (const item of waitingItems) {
|
|
2582
|
+
const diff = item.nextRunAt - now;
|
|
2583
|
+
if (diff < minWaitMs) minWaitMs = diff;
|
|
2463
2584
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
// Smart gambling: skip gamble commands while loss-paused
|
|
2467
|
-
const GAMBLE_SET = new Set(['blackjack', 'cointoss', 'roulette', 'slots', 'snakeeyes']);
|
|
2468
|
-
if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > now) {
|
|
2469
|
-
item.nextRunAt = this._gamblePausedUntil;
|
|
2470
|
-
if (this.commandQueue) this.commandQueue.push(item);
|
|
2471
|
-
this.setStatus(`gamble paused (${Math.ceil((this._gamblePausedUntil - now) / 1000)}s)`);
|
|
2472
|
-
this.tickTimeout = setTimeout(() => this.tick(), 100);
|
|
2585
|
+
this.setStatus('cooldown...');
|
|
2586
|
+
this.tickTimeout = setTimeout(() => this.tick(), Math.max(100, Math.min(minWaitMs, 5000)));
|
|
2473
2587
|
return;
|
|
2474
2588
|
}
|
|
2475
|
-
if (GAMBLE_SET.has(item.cmd) && this._gamblePausedUntil > 0 && this._gamblePausedUntil <= now) {
|
|
2476
|
-
this._gamblePausedUntil = 0;
|
|
2477
|
-
this._gambleLossStreak = 0;
|
|
2478
|
-
this._gambleSessionLoss = 0;
|
|
2479
|
-
this.log('info', 'Gambling pause expired — resuming bets');
|
|
2480
|
-
}
|
|
2481
2589
|
|
|
2482
|
-
//
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2590
|
+
// FIFO — execute commands in the order they became ready.
|
|
2591
|
+
// Commands that aren't picked this tick go back into the queue for next tick.
|
|
2592
|
+
const item = readyItems[0];
|
|
2593
|
+
|
|
2594
|
+
// Any remaining ready items that we didn't execute go back immediately
|
|
2595
|
+
// so they run in the next tick without waiting for the slow top item.
|
|
2596
|
+
for (let i = 1; i < readyItems.length; i++) {
|
|
2597
|
+
this.commandQueue.push(readyItems[i]);
|
|
2489
2598
|
}
|
|
2490
2599
|
|
|
2491
|
-
this
|
|
2492
|
-
this.busy = true;
|
|
2600
|
+
// Anti-detection: per-account jitter + micro-pauses for this command
|
|
2493
2601
|
const cd = (this.account[item.info.cdKey] || item.info.defaultCd);
|
|
2494
|
-
// Anti-detection: per-account jitter with varying patterns
|
|
2495
2602
|
const patternMod = this._activePattern;
|
|
2496
2603
|
const jitterBase = cd <= 5
|
|
2497
2604
|
? 0.3 + Math.random() * 0.7
|
|
2498
2605
|
: cd <= 20
|
|
2499
2606
|
? 0.5 + Math.random() * 1.5
|
|
2500
2607
|
: 1 + Math.random() * 2;
|
|
2501
|
-
// Add human-like micro-pauses (occasionally take longer, simulating distraction)
|
|
2502
2608
|
const microPause = Math.random() < 0.08 ? 1.5 + Math.random() * 3 : 0;
|
|
2503
2609
|
const totalWait = cd + jitterBase + microPause;
|
|
2504
2610
|
|
|
@@ -2510,6 +2616,20 @@ class AccountWorker {
|
|
|
2510
2616
|
await new Promise(r => setTimeout(r, minGap - timeSinceLastCmd));
|
|
2511
2617
|
}
|
|
2512
2618
|
|
|
2619
|
+
// TokenBucket rate limiter — prevent Discord 429s
|
|
2620
|
+
if (!this._rateLimiter.consume(1)) {
|
|
2621
|
+
const waitMs = this._rateLimiter.waitTime(1);
|
|
2622
|
+
this.setStatus(`rate throttle (${Math.ceil(waitMs / 1000)}s)`);
|
|
2623
|
+
if (this.commandQueue) this.commandQueue.push(item);
|
|
2624
|
+
this.tickTimeout = setTimeout(() => this.tick(), waitMs);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
this.busy = true;
|
|
2629
|
+
|
|
2630
|
+
// ── Run command (with interactive retry) ───────────────────
|
|
2631
|
+
// Commands run ONE BY ONE — sequential execution, no concurrency within this account.
|
|
2632
|
+
// Each runCommand() call waits for Dank Memer's Discord response before returning.
|
|
2513
2633
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
2514
2634
|
this.setStatus(formatCommandName(item.cmd));
|
|
2515
2635
|
|
|
@@ -2523,15 +2643,40 @@ class AccountWorker {
|
|
|
2523
2643
|
next_run_at: nextItemRun?.nextRunAt || null,
|
|
2524
2644
|
});
|
|
2525
2645
|
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
const
|
|
2646
|
+
// Interactive commands (button-click): retry up to 3 times on failure.
|
|
2647
|
+
// Non-interactive commands run once.
|
|
2648
|
+
const INTERACTIVE_CMDS = new Set(['hl', 'blackjack', 'trivia', 'adventure', 'stream', 'fish', 'farm', 'work shift']);
|
|
2649
|
+
const isInteractive = INTERACTIVE_CMDS.has(item.cmd);
|
|
2650
|
+
const maxAttempts = isInteractive ? 3 : 1;
|
|
2651
|
+
let attempt = 0;
|
|
2652
|
+
let lastError = null;
|
|
2653
|
+
let earned = 0;
|
|
2654
|
+
while (attempt < maxAttempts) {
|
|
2655
|
+
attempt++;
|
|
2656
|
+
try {
|
|
2657
|
+
const beforeCoins = this.stats.coins;
|
|
2658
|
+
await this.runCommand(item.cmd, prefix);
|
|
2659
|
+
earned = this.stats.coins - beforeCoins;
|
|
2660
|
+
lastError = null;
|
|
2661
|
+
this.lastCommandRun = Date.now();
|
|
2662
|
+
break; // success
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
lastError = err;
|
|
2665
|
+
this.log('warn', `${item.cmd} attempt ${attempt}/${maxAttempts} failed: ${err.message}`);
|
|
2666
|
+
if (attempt < maxAttempts) {
|
|
2667
|
+
await new Promise(r => setTimeout(r, 1000 + Math.random() * 1000));
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (lastError) {
|
|
2672
|
+
this.log('error', `${item.cmd} failed after ${maxAttempts} attempts — skipping`);
|
|
2673
|
+
}
|
|
2529
2674
|
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
//
|
|
2533
|
-
|
|
2534
|
-
if (
|
|
2675
|
+
this._cmdRate.increment();
|
|
2676
|
+
|
|
2677
|
+
// Grace period for interactive commands — Dank Memer needs time to process
|
|
2678
|
+
// the interaction before accepting the next command.
|
|
2679
|
+
if (isInteractive) {
|
|
2535
2680
|
await new Promise(r => setTimeout(r, 2500 + Math.random() * 1500));
|
|
2536
2681
|
}
|
|
2537
2682
|
|
|
@@ -2549,8 +2694,6 @@ class AccountWorker {
|
|
|
2549
2694
|
this.failStreak = 0;
|
|
2550
2695
|
}
|
|
2551
2696
|
|
|
2552
|
-
this.lastCommandRun = Date.now();
|
|
2553
|
-
|
|
2554
2697
|
// Exponential backoff: if too many consecutive failures, slow down
|
|
2555
2698
|
const backoffMultiplier = this.failStreak > 5 ? Math.min(this.failStreak - 4, 5) : 1;
|
|
2556
2699
|
// Minimum 5s cooldown for failed commands to prevent rapid-fire retries
|
|
@@ -2558,22 +2701,33 @@ class AccountWorker {
|
|
|
2558
2701
|
|
|
2559
2702
|
if (this.commandQueue && this.running && !shutdownCalled) {
|
|
2560
2703
|
const hasOverride = Number.isFinite(this._lastCooldownOverride) && this._lastCooldownOverride > 0;
|
|
2561
|
-
let effectiveWait = hasOverride ? this._lastCooldownOverride : totalWait;
|
|
2562
2704
|
this._lastCooldownOverride = null;
|
|
2563
2705
|
|
|
2564
|
-
|
|
2565
|
-
if (
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2706
|
+
let scheduledWaitSec;
|
|
2707
|
+
if (hasOverride) {
|
|
2708
|
+
// Exact cooldown returned by command parser — use it without jitter or backoff.
|
|
2709
|
+
// This ensures work (1h), adventure (5h), stream (10m), farm (varies) are honored exactly.
|
|
2710
|
+
scheduledWaitSec = Math.max(1, this._lastCooldownOverride);
|
|
2711
|
+
} else {
|
|
2712
|
+
// Jitter-based cooldown for commands without a parsed override.
|
|
2713
|
+
let effectiveWait = totalWait;
|
|
2714
|
+
|
|
2715
|
+
// Smart fallback floors: static floor OR dynamically learned cooldown (whichever is higher).
|
|
2716
|
+
// Learned cooldowns come from actual parsed responses and persist across restarts via Redis.
|
|
2717
|
+
const staticFloor = AccountWorker.SMART_CD_FLOORS[item.cmd] || 0;
|
|
2718
|
+
const learnedCd = this._learnedCooldowns.get(item.cmd) || 0;
|
|
2719
|
+
const adaptiveFloor = Math.max(staticFloor, learnedCd);
|
|
2720
|
+
if (adaptiveFloor > 0) {
|
|
2721
|
+
effectiveWait = Math.max(effectiveWait, adaptiveFloor);
|
|
2569
2722
|
}
|
|
2570
|
-
}
|
|
2571
2723
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2724
|
+
if (earned <= 0 && !noFailCmds.includes(item.cmd) && effectiveWait < MIN_FAIL_COOLDOWN) {
|
|
2725
|
+
effectiveWait = MIN_FAIL_COOLDOWN;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
|
|
2574
2729
|
}
|
|
2575
2730
|
|
|
2576
|
-
const scheduledWaitSec = Math.max(1, effectiveWait * backoffMultiplier);
|
|
2577
2731
|
await this.setCooldown(item.cmd, scheduledWaitSec);
|
|
2578
2732
|
item.nextRunAt = Date.now() + scheduledWaitSec * 1000;
|
|
2579
2733
|
this.commandQueue.push(item);
|
|
@@ -2611,7 +2765,8 @@ class AccountWorker {
|
|
|
2611
2765
|
this.failStreak = 0;
|
|
2612
2766
|
this.cycleCount = 0;
|
|
2613
2767
|
this.lastCommandRun = 0;
|
|
2614
|
-
|
|
2768
|
+
await this._loadLearnedCooldowns();
|
|
2769
|
+
this.commandQueue = await this.buildCommandQueue();
|
|
2615
2770
|
this.lastHealthCheck = Date.now();
|
|
2616
2771
|
|
|
2617
2772
|
// Reactive alert listener: run `pls alert` only when Dank Memer
|
|
@@ -2713,11 +2868,28 @@ class AccountWorker {
|
|
|
2713
2868
|
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2714
2869
|
body: JSON.stringify({ action_id: action.id }),
|
|
2715
2870
|
});
|
|
2716
|
-
} catch {
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
console.error(`[${this.username}] delete pending action error:`, err?.message || err);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (action.action === 'check_profile' && !this.busy) {
|
|
2876
|
+
this.log('info', 'Dashboard requested profile check');
|
|
2877
|
+
await this.checkProfile().catch(() => {});
|
|
2878
|
+
try {
|
|
2879
|
+
await fetch(`${API_URL}/api/grinder/actions`, {
|
|
2880
|
+
method: 'DELETE',
|
|
2881
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2882
|
+
body: JSON.stringify({ action_id: action.id }),
|
|
2883
|
+
});
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
console.error(`[${this.username}] delete pending action error:`, err?.message || err);
|
|
2886
|
+
}
|
|
2717
2887
|
}
|
|
2718
2888
|
}
|
|
2719
2889
|
}
|
|
2720
|
-
} catch {
|
|
2890
|
+
} catch (err) {
|
|
2891
|
+
console.error(`[${this.username}] refreshConfig error:`, err?.message || err);
|
|
2892
|
+
}
|
|
2721
2893
|
}
|
|
2722
2894
|
|
|
2723
2895
|
async start() {
|
|
@@ -2767,9 +2939,8 @@ class AccountWorker {
|
|
|
2767
2939
|
{ key: 'cmd_fish', l: 'fish' }, { key: 'cmd_beg', l: 'beg' },
|
|
2768
2940
|
{ key: 'cmd_search', l: 'search' }, { key: 'cmd_hl', l: 'hl' },
|
|
2769
2941
|
{ key: 'cmd_crime', l: 'crime' }, { key: 'cmd_pm', l: 'pm' },
|
|
2770
|
-
{ key: 'cmd_daily', l: 'daily' }, { key: '
|
|
2771
|
-
{ key: '
|
|
2772
|
-
{ key: 'cmd_stream', l: 'stream' }, { key: 'cmd_scratch', l: 'scratch' },
|
|
2942
|
+
{ key: 'cmd_daily', l: 'daily' }, { key: 'cmd_monthly', l: 'monthly' },
|
|
2943
|
+
{ key: 'cmd_work', l: 'work' }, { key: 'cmd_stream', l: 'stream' },
|
|
2773
2944
|
{ key: 'cmd_adventure', l: 'adv' }, { key: 'cmd_farm', l: 'farm' },
|
|
2774
2945
|
{ key: 'cmd_tidy', l: 'tidy' }, { key: 'cmd_blackjack', l: 'bj' },
|
|
2775
2946
|
{ key: 'cmd_cointoss', l: 'toss' }, { key: 'cmd_roulette', l: 'roul' },
|
|
@@ -3048,9 +3219,30 @@ async function start(apiKey, apiUrl) {
|
|
|
3048
3219
|
}
|
|
3049
3220
|
loginLines.push(` ${'─'.repeat(loginVis)}`);
|
|
3050
3221
|
for (const l of loginLines) console.log(l);
|
|
3051
|
-
|
|
3222
|
+
|
|
3223
|
+
// Dynamically capture the starting row of the login table via DSR
|
|
3224
|
+
let loginBaseRow = 1;
|
|
3225
|
+
const captureLoginRow = () => new Promise(resolve => {
|
|
3226
|
+
process.stdout.write(MARKER);
|
|
3227
|
+
const chunks = [];
|
|
3228
|
+
const handler = (chunk) => {
|
|
3229
|
+
chunks.push(chunk);
|
|
3230
|
+
const raw = chunks.join('');
|
|
3231
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3232
|
+
if (m) {
|
|
3233
|
+
process.stdin.removeListener('data', handler);
|
|
3234
|
+
loginBaseRow = parseInt(m[1], 10) + 1;
|
|
3235
|
+
resolve();
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
process.stdin.on('data', handler);
|
|
3239
|
+
setTimeout(resolve, 50);
|
|
3240
|
+
});
|
|
3241
|
+
await captureLoginRow();
|
|
3052
3242
|
|
|
3053
3243
|
let loginPending = new Array(accounts.length).fill(true);
|
|
3244
|
+
const moveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3245
|
+
|
|
3054
3246
|
const drawLoginSpinners = () => {
|
|
3055
3247
|
for (let i = 0; i < loginPending.length; i++) {
|
|
3056
3248
|
if (!loginPending[i]) continue;
|
|
@@ -3059,8 +3251,13 @@ async function start(apiKey, apiUrl) {
|
|
|
3059
3251
|
const name = loginStates[i].name.substring(0, colName).padEnd(colName);
|
|
3060
3252
|
const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
|
|
3061
3253
|
const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
|
|
3062
|
-
|
|
3254
|
+
const row = loginBaseRow + 1 + i; // +1 skips the top border line
|
|
3255
|
+
moveToRow(row);
|
|
3256
|
+
process.stdout.write(` ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
|
|
3063
3257
|
}
|
|
3258
|
+
// Move cursor back to bottom to avoid overwriting the bottom border
|
|
3259
|
+
const lastRow = loginBaseRow + 1 + accounts.length + 1;
|
|
3260
|
+
moveToRow(lastRow);
|
|
3064
3261
|
};
|
|
3065
3262
|
const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
|
|
3066
3263
|
|
|
@@ -3089,7 +3286,9 @@ async function start(apiKey, apiUrl) {
|
|
|
3089
3286
|
guild = 'timeout'.padEnd(colGuild);
|
|
3090
3287
|
cmds = '···'.padEnd(colCmds);
|
|
3091
3288
|
}
|
|
3092
|
-
|
|
3289
|
+
const row = loginBaseRow + 1 + idx; // +1 skips the top border line
|
|
3290
|
+
moveToRow(row);
|
|
3291
|
+
process.stdout.write(` ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
|
|
3093
3292
|
};
|
|
3094
3293
|
|
|
3095
3294
|
const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
|
|
@@ -3137,6 +3336,28 @@ async function start(apiKey, apiUrl) {
|
|
|
3137
3336
|
const iColVal = 16;
|
|
3138
3337
|
const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
|
|
3139
3338
|
|
|
3339
|
+
// Print a unique marker, query its position, then overwrite it with the table
|
|
3340
|
+
process.stdout.write(MARKER);
|
|
3341
|
+
let invBaseRow = 1;
|
|
3342
|
+
const captureRow = () => new Promise(resolve => {
|
|
3343
|
+
const chunks = [];
|
|
3344
|
+
const handler = (chunk) => {
|
|
3345
|
+
chunks.push(chunk);
|
|
3346
|
+
const raw = chunks.join('');
|
|
3347
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3348
|
+
if (m) {
|
|
3349
|
+
process.stdin.removeListener('data', handler);
|
|
3350
|
+
invBaseRow = parseInt(m[1], 10) + 1; // +1: first account row is after marker
|
|
3351
|
+
resolve();
|
|
3352
|
+
}
|
|
3353
|
+
};
|
|
3354
|
+
process.stdin.on('data', handler);
|
|
3355
|
+
setTimeout(resolve, 50);
|
|
3356
|
+
});
|
|
3357
|
+
await captureRow();
|
|
3358
|
+
|
|
3359
|
+
// Now print the inventory table starting at invBaseRow
|
|
3360
|
+
const invMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3140
3361
|
console.log(` ${'─'.repeat(invVis)}`);
|
|
3141
3362
|
for (let i = 0; i < activeWorkers.length; i++) {
|
|
3142
3363
|
const w = activeWorkers[i];
|
|
@@ -3155,7 +3376,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3155
3376
|
const filled = Math.round(pct * barW);
|
|
3156
3377
|
const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
|
|
3157
3378
|
const pctStr = `${Math.round(pct * 100)}%`;
|
|
3158
|
-
|
|
3379
|
+
invMoveToRow(invBaseRow);
|
|
3380
|
+
process.stdout.write(` ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - invPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}${pctStr}${c.reset} \x1b[K`);
|
|
3159
3381
|
};
|
|
3160
3382
|
const invSpinnerInterval = setInterval(drawInvProgress, 80);
|
|
3161
3383
|
|
|
@@ -3171,7 +3393,9 @@ async function start(apiKey, apiUrl) {
|
|
|
3171
3393
|
const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
|
|
3172
3394
|
const itemStr = `${items}`.padEnd(iColItems);
|
|
3173
3395
|
const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
|
|
3174
|
-
|
|
3396
|
+
const row = invBaseRow + 1 + i;
|
|
3397
|
+
invMoveToRow(row);
|
|
3398
|
+
process.stdout.write(` ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
|
|
3175
3399
|
if (invRes?.ok) invDone++; else invFailed++;
|
|
3176
3400
|
}));
|
|
3177
3401
|
|
|
@@ -3195,6 +3419,27 @@ async function start(apiKey, apiUrl) {
|
|
|
3195
3419
|
const bColLs = 4;
|
|
3196
3420
|
const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
|
|
3197
3421
|
|
|
3422
|
+
// Capture starting row for balance phase
|
|
3423
|
+
process.stdout.write(MARKER);
|
|
3424
|
+
let balBaseRow = 1;
|
|
3425
|
+
const balCaptureRow = () => new Promise(resolve => {
|
|
3426
|
+
const chunks = [];
|
|
3427
|
+
const handler = (chunk) => {
|
|
3428
|
+
chunks.push(chunk);
|
|
3429
|
+
const raw = chunks.join('');
|
|
3430
|
+
const m = raw.match(/\x1b\[(\d+);\d+R/);
|
|
3431
|
+
if (m) {
|
|
3432
|
+
process.stdin.removeListener('data', handler);
|
|
3433
|
+
balBaseRow = parseInt(m[1], 10) + 1;
|
|
3434
|
+
resolve();
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
process.stdin.on('data', handler);
|
|
3438
|
+
setTimeout(resolve, 50);
|
|
3439
|
+
});
|
|
3440
|
+
await balCaptureRow();
|
|
3441
|
+
|
|
3442
|
+
const balMoveToRow = (row) => process.stdout.write(`\x1b[${row};1H`);
|
|
3198
3443
|
console.log(` ${'─'.repeat(balVis)}`);
|
|
3199
3444
|
for (let i = 0; i < activeWorkers.length; i++) {
|
|
3200
3445
|
const w = activeWorkers[i];
|
|
@@ -3212,7 +3457,8 @@ async function start(apiKey, apiUrl) {
|
|
|
3212
3457
|
const barW = Math.min(20, startupTw - 40);
|
|
3213
3458
|
const filled = Math.round(pct * barW);
|
|
3214
3459
|
const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
|
|
3215
|
-
|
|
3460
|
+
balMoveToRow(balBaseRow);
|
|
3461
|
+
process.stdout.write(` ${rgb(251, 191, 36)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - balPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} \x1b[K`);
|
|
3216
3462
|
};
|
|
3217
3463
|
const balSpinnerInterval = setInterval(drawBalProgress, 80);
|
|
3218
3464
|
|
|
@@ -3228,7 +3474,9 @@ async function start(apiKey, apiUrl) {
|
|
|
3228
3474
|
const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
|
|
3229
3475
|
const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
|
|
3230
3476
|
const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
|
|
3231
|
-
|
|
3477
|
+
const row = balBaseRow + 1 + i;
|
|
3478
|
+
balMoveToRow(row);
|
|
3479
|
+
process.stdout.write(` ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}\x1b[K`);
|
|
3232
3480
|
balDone++;
|
|
3233
3481
|
}));
|
|
3234
3482
|
|