dankgrinder 7.1.0 → 7.7.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/farm.js +235 -31
- package/lib/commands/farmVision.js +198 -140
- package/lib/commands/utils.js +18 -4
- package/lib/grinder.js +65 -16
- package/lib/rawLogger.js +229 -102
- package/package.json +1 -1
package/lib/commands/farm.js
CHANGED
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
isCV2, ensureCV2, stripAnsi, needsItem, clickCV2SelectMenu,
|
|
5
5
|
} = require('./utils');
|
|
6
6
|
const { buyItem, buyItemsBatch } = require('./shop');
|
|
7
|
+
const rawLogger = require('../../lib/rawLogger');
|
|
7
8
|
const {
|
|
8
9
|
downloadImage,
|
|
9
10
|
extractFarmImageUrl,
|
|
@@ -474,8 +475,8 @@ function getVisionRepairPlan(actionName, score) {
|
|
|
474
475
|
}
|
|
475
476
|
if (actionName === 'hoe') {
|
|
476
477
|
const planted = score.ratios?.planted || 0;
|
|
477
|
-
const
|
|
478
|
-
if (
|
|
478
|
+
const tilled = score.ratios?.tilled || 0;
|
|
479
|
+
if (planted >= 0.30 || tilled >= 0.30) {
|
|
479
480
|
// Farm likely already progressed beyond hoe stage.
|
|
480
481
|
return null;
|
|
481
482
|
}
|
|
@@ -488,10 +489,9 @@ function getVisionRepairPlan(actionName, score) {
|
|
|
488
489
|
function inferNextActionFromScore(actionName, score) {
|
|
489
490
|
if (!score) return null;
|
|
490
491
|
const planted = score.ratios?.planted || 0;
|
|
491
|
-
const wet = score.ratios?.wet || 0;
|
|
492
492
|
const tilled = score.ratios?.tilled || 0;
|
|
493
493
|
|
|
494
|
-
if (actionName === 'hoe' && (planted +
|
|
494
|
+
if (actionName === 'hoe' && (planted + tilled) >= 0.45) {
|
|
495
495
|
return 'water';
|
|
496
496
|
}
|
|
497
497
|
if (actionName === 'water' && planted >= 0.45) {
|
|
@@ -515,16 +515,14 @@ async function inferPreferredActionFromImage(msg) {
|
|
|
515
515
|
const slots = Math.max(1, (analysis?.rows || 3) * (analysis?.cols || 3));
|
|
516
516
|
const ratios = {
|
|
517
517
|
tilled: (analysis?.counts?.tilled || 0) / slots,
|
|
518
|
-
wet: (analysis?.counts?.wet || 0) / slots,
|
|
519
518
|
planted: (analysis?.counts?.planted || 0) / slots,
|
|
520
519
|
unknown: (analysis?.counts?.unknown || 0) / slots,
|
|
521
520
|
};
|
|
522
521
|
|
|
523
522
|
let suggested = null;
|
|
524
|
-
// Image-first phase routing.
|
|
525
|
-
if (ratios.planted >= 0.
|
|
526
|
-
else if (ratios.
|
|
527
|
-
else if (ratios.tilled >= 0.35) suggested = 'water';
|
|
523
|
+
// Image-first phase routing (3-state: tilled/planted/unknown, no separate wet state).
|
|
524
|
+
if (ratios.planted >= 0.30) suggested = 'plant';
|
|
525
|
+
else if (ratios.tilled >= 0.30) suggested = 'water';
|
|
528
526
|
else suggested = 'hoe';
|
|
529
527
|
|
|
530
528
|
let dbg = null;
|
|
@@ -541,7 +539,6 @@ async function inferPreferredActionFromImage(msg) {
|
|
|
541
539
|
|
|
542
540
|
LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} counts=${JSON.stringify(analysis.counts)} conf=${analysis.avgConfidence} suggested=${suggested} ratios=${JSON.stringify({
|
|
543
541
|
tilled: +ratios.tilled.toFixed(3),
|
|
544
|
-
wet: +ratios.wet.toFixed(3),
|
|
545
542
|
planted: +ratios.planted.toFixed(3),
|
|
546
543
|
unknown: +ratios.unknown.toFixed(3),
|
|
547
544
|
})}`);
|
|
@@ -942,6 +939,17 @@ function pickFarmActionButton(msg, text) {
|
|
|
942
939
|
return null;
|
|
943
940
|
}
|
|
944
941
|
|
|
942
|
+
// Captures the ephemeral interaction response content from a CV2 ack object.
|
|
943
|
+
// Dank Memer sends error/success text in the interaction ACK (type 4/7) content field.
|
|
944
|
+
function captureCvvAckContent(ack) {
|
|
945
|
+
if (!ack) return '';
|
|
946
|
+
const flags = ack?.flags ?? ack?.data?.flags ?? 0;
|
|
947
|
+
const content = String(stripAnsi(ack?.content || ack?.data?.content || '')).trim();
|
|
948
|
+
if (!content) return '';
|
|
949
|
+
const isEphemeral = (flags & 64) !== 0;
|
|
950
|
+
return `[ephemeral=${isEphemeral}] ${content}`;
|
|
951
|
+
}
|
|
952
|
+
|
|
945
953
|
async function clickAndCapture({ channel, waitForDankMemer, response, button, tag, timeoutMs = 9000 }) {
|
|
946
954
|
const baseline = brief(getFullText(response), 400);
|
|
947
955
|
const clickRes = await safeClickButton(response, button);
|
|
@@ -951,7 +959,30 @@ async function clickAndCapture({ channel, waitForDankMemer, response, button, ta
|
|
|
951
959
|
delete response._lastInteractionAck;
|
|
952
960
|
}
|
|
953
961
|
|
|
954
|
-
|
|
962
|
+
// CV2 HTTP fallback returns the interaction ACK (not the updated message).
|
|
963
|
+
// If clickRes looks like an ack rather than a message, extract it and wait for
|
|
964
|
+
// the edited message instead.
|
|
965
|
+
const isAckObject = clickRes && typeof clickRes === 'object' && !clickRes.id && (clickRes.interactionStatus || clickRes.interactionType);
|
|
966
|
+
const capturedAck = isAckObject ? clickRes : null;
|
|
967
|
+
|
|
968
|
+
// Register a one-time callback to capture ephemeral responses (e.g. "You harvested: Nothing")
|
|
969
|
+
// from the raw gateway packet — the only place where ephemeral embeds/CV2 text are preserved.
|
|
970
|
+
let capturedEphemeral = null;
|
|
971
|
+
rawLogger.onNextEphemeral((parsed) => {
|
|
972
|
+
capturedEphemeral = parsed;
|
|
973
|
+
const cv2t = parsed.cv2Text || '';
|
|
974
|
+
const allt = parsed.allText || '';
|
|
975
|
+
const txt = cv2t || allt;
|
|
976
|
+
if (txt.includes('harvested')) LOG.info(`${tag}-ephemeral harvest: ${txt.slice(0, 200)}`);
|
|
977
|
+
else LOG.info(`${tag}-ephemeral ${txt.slice(0, 200)}`);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// Extra grace period: the ephemeral often arrives after safeClickButton resolves
|
|
981
|
+
// (the library may return the stale message immediately, before the gateway delivers the
|
|
982
|
+
// ephemeral). This ensures we capture it even when it arrives late.
|
|
983
|
+
await sleep(1500);
|
|
984
|
+
|
|
985
|
+
let post = (!isAckObject && clickRes) ? clickRes : null;
|
|
955
986
|
if (!post && response.id) post = await waitForEditedMessage(channel, response.id, baseline, timeoutMs);
|
|
956
987
|
if (!post) post = await waitForDankMemer(timeoutMs);
|
|
957
988
|
|
|
@@ -960,17 +991,23 @@ async function clickAndCapture({ channel, waitForDankMemer, response, button, ta
|
|
|
960
991
|
|
|
961
992
|
// Capture any additional immediate callback message (often ephemeral-like
|
|
962
993
|
// "only you can see this" notices) that may be separate from edited CV2 post.
|
|
994
|
+
let extraMsg = null;
|
|
963
995
|
try {
|
|
964
996
|
const side = await waitForDankMemer(1500);
|
|
965
997
|
if (side && side.id !== post.id) {
|
|
966
998
|
logEphemeralLike(`${tag}-post-extra`, side);
|
|
967
|
-
|
|
999
|
+
extraMsg = side;
|
|
968
1000
|
}
|
|
969
1001
|
} catch {}
|
|
970
1002
|
|
|
971
1003
|
if (isCV2(post)) await ensureCV2(post);
|
|
972
1004
|
logMsg(post, tag);
|
|
973
1005
|
logFarmState(tag, post);
|
|
1006
|
+
|
|
1007
|
+
// Attach captured ack and extra message so callers can access them.
|
|
1008
|
+
post._capturedAck = capturedAck;
|
|
1009
|
+
post._farmExtraMsg = extraMsg;
|
|
1010
|
+
post._capturedEphemeral = capturedEphemeral;
|
|
974
1011
|
return post;
|
|
975
1012
|
}
|
|
976
1013
|
|
|
@@ -1117,31 +1154,30 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1117
1154
|
const slots = Math.max(1, (imageAnalysis.rows || 3) * (imageAnalysis.cols || 3));
|
|
1118
1155
|
const ratios = {
|
|
1119
1156
|
tilled: (imageAnalysis.counts?.tilled || 0) / slots,
|
|
1120
|
-
wet: (imageAnalysis.counts?.wet || 0) / slots,
|
|
1121
1157
|
planted: (imageAnalysis.counts?.planted || 0) / slots,
|
|
1122
1158
|
unknown: (imageAnalysis.counts?.unknown || 0) / slots,
|
|
1123
1159
|
};
|
|
1124
1160
|
|
|
1125
|
-
// Phase-aware selection: pick the earliest incomplete phase.
|
|
1126
|
-
// Harvest when planted >=
|
|
1127
|
-
if (ratios.planted >= 0.
|
|
1161
|
+
// Phase-aware selection: pick the earliest incomplete phase (3-state model).
|
|
1162
|
+
// Harvest when planted >= 30%.
|
|
1163
|
+
if (ratios.planted >= 0.30) {
|
|
1128
1164
|
const btn = managedActions.harvest || btns.find(b => hasAny(b, ['harvest', 'reap', 'collect']));
|
|
1129
1165
|
if (btn) return { action: 'harvest', button: btn, reason: `harvest-ready-vision(planted=${ratios.planted.toFixed(2)})` };
|
|
1130
1166
|
}
|
|
1131
|
-
// Plant when
|
|
1132
|
-
if (ratios.
|
|
1167
|
+
// Plant when tilled >= 30%.
|
|
1168
|
+
if (ratios.tilled >= 0.30) {
|
|
1133
1169
|
const btn = managedActions.plant || btns.find(b => hasAny(b, ['plant', 'seed', 'sow']));
|
|
1134
|
-
if (btn) return { action: 'plant', button: btn, reason: `plant-vision(
|
|
1170
|
+
if (btn) return { action: 'plant', button: btn, reason: `plant-vision(tilled=${ratios.tilled.toFixed(2)})` };
|
|
1135
1171
|
}
|
|
1136
|
-
// Water when we have tilled tiles
|
|
1137
|
-
if (ratios.tilled >= 0.
|
|
1172
|
+
// Water when we have tilled tiles.
|
|
1173
|
+
if (ratios.tilled >= 0.15) {
|
|
1138
1174
|
const btn = managedActions.water || btns.find(b => hasAny(b, ['water', 'watering']));
|
|
1139
|
-
if (btn) return { action: 'water', button: btn, reason: `water-vision(tilled=${ratios.tilled.toFixed(2)}
|
|
1175
|
+
if (btn) return { action: 'water', button: btn, reason: `water-vision(tilled=${ratios.tilled.toFixed(2)})` };
|
|
1140
1176
|
}
|
|
1141
1177
|
// Hoe when farm is mostly unknown/empty.
|
|
1142
|
-
if (ratios.tilled < 0.
|
|
1178
|
+
if (ratios.tilled < 0.15 && ratios.planted < 0.30) {
|
|
1143
1179
|
const btn = managedActions.hoe || btns.find(b => hasAny(b, ['hoe', 'till']));
|
|
1144
|
-
if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},
|
|
1180
|
+
if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},p=${ratios.planted.toFixed(2)})` };
|
|
1145
1181
|
}
|
|
1146
1182
|
}
|
|
1147
1183
|
|
|
@@ -1177,11 +1213,20 @@ async function advancePastConfirmation(response, waitForDankMemer) {
|
|
|
1177
1213
|
if (!response) return null;
|
|
1178
1214
|
|
|
1179
1215
|
const CONFIRM_WORDS = ['continue', 'confirm', 'confirm all', 'done', 'back to farm', 'back', 'close'];
|
|
1216
|
+
// Also treat "All" buttons as confirmation actions (e.g. "Hoe All", "Water All").
|
|
1217
|
+
// Skip the plain "back" word to avoid matching the Back arrow button; prefer explicit
|
|
1218
|
+
// confirm words first, then fall back to "All" buttons as confirmation.
|
|
1219
|
+
const ALL_AS_CONFIRM = ['hoe all', 'water all', 'plant all', 'harvest all', 'fertilize all'];
|
|
1180
1220
|
const MAX_PAGES = 3;
|
|
1181
1221
|
|
|
1182
1222
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
1183
1223
|
const btns = getAllButtons(response).filter(b => !b.disabled);
|
|
1184
|
-
|
|
1224
|
+
let confirmBtn = btns.find(b => CONFIRM_WORDS.some(w => buttonHay(b).includes(w)));
|
|
1225
|
+
// Also treat "All" buttons as confirmation (e.g. clicking "Plant All" again on the
|
|
1226
|
+
// confirmation screen advances past it). But prefer explicit confirm words first.
|
|
1227
|
+
if (!confirmBtn) {
|
|
1228
|
+
confirmBtn = btns.find(b => ALL_AS_CONFIRM.some(w => buttonHay(b).includes(w)));
|
|
1229
|
+
}
|
|
1185
1230
|
|
|
1186
1231
|
if (!confirmBtn) {
|
|
1187
1232
|
// No more confirmation buttons — return the current screen (should be manage menu)
|
|
@@ -1197,6 +1242,18 @@ async function advancePastConfirmation(response, waitForDankMemer) {
|
|
|
1197
1242
|
try {
|
|
1198
1243
|
clicked = await safeClickButton(response, confirmBtn);
|
|
1199
1244
|
logEphemeralLike(`confirm-page-${page}`, clicked);
|
|
1245
|
+
// Preserve CV2 ack so cycle can check for ephemeral errors.
|
|
1246
|
+
if (clicked?._lastInteractionAck) {
|
|
1247
|
+
response._lastInteractionAck = clicked._lastInteractionAck;
|
|
1248
|
+
}
|
|
1249
|
+
// Also capture ephemeral ack content for logging
|
|
1250
|
+
const ackContent = captureCvvAckContent(clicked);
|
|
1251
|
+
if (ackContent) {
|
|
1252
|
+
const flags = clicked?.flags ?? 0;
|
|
1253
|
+
const isEphemeral = (flags & 64) !== 0;
|
|
1254
|
+
const level = isEphemeral ? LOG.warn : LOG.info;
|
|
1255
|
+
level(`[farm:confirm:${page}] ephemeral=${isEphemeral} ack="${ackContent.slice(0, 300)}"`);
|
|
1256
|
+
}
|
|
1200
1257
|
} catch (e) {
|
|
1201
1258
|
LOG.warn(`[farm:confirm] click failed on page ${page}: ${e.message}`);
|
|
1202
1259
|
return response;
|
|
@@ -1240,7 +1297,7 @@ async function advancePastConfirmation(response, waitForDankMemer) {
|
|
|
1240
1297
|
// ── Single-cycle farm orchestrator ──────────────────────────────────────────
|
|
1241
1298
|
// Sends `pls farm view` once, then completes the full hoe→water→plant→harvest
|
|
1242
1299
|
// cycle by looping on the returned manage menu — no additional command sends.
|
|
1243
|
-
async function runFarm({ channel, waitForDankMemer, client, redis, accountId }) {
|
|
1300
|
+
async function runFarm({ channel, waitForDankMemer, client, redis, accountId, forceRun }) {
|
|
1244
1301
|
LOG.cmd(`${c.white}${c.bold}pls farm view${c.reset}`);
|
|
1245
1302
|
|
|
1246
1303
|
await channel.send('pls farm view');
|
|
@@ -1296,7 +1353,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1296
1353
|
if (textHasHarvestReady) LOG.info(`[farm] DETECTED harvest-ready in text (growReadySec=${growReadySec})`);
|
|
1297
1354
|
if (growReadySec > 0) LOG.info(`[farm] GROW-QUEUE DEBUG: lower=${lower.slice(0, 400)}`);
|
|
1298
1355
|
LOG.info(`[farm] grow-ready parse=${growReadySec == null ? 'none' : `${growReadySec}s`} redis_recovery=${redisRecoveryMs != null ? `${redisRecoveryMs}ms` : 'n/a'}`);
|
|
1299
|
-
if (!redisRecovered && growReadySec && growReadySec > 20) {
|
|
1356
|
+
if (!forceRun && !redisRecovered && growReadySec && growReadySec > 20) {
|
|
1300
1357
|
await inferPreferredActionFromImage(response);
|
|
1301
1358
|
// Re-check every 30s instead of waiting the full grow duration.
|
|
1302
1359
|
// This ensures the next harvest cycle starts as soon as crops are ready.
|
|
@@ -1433,6 +1490,30 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1433
1490
|
label.includes('plant all') || label.includes('harvest all') || label.includes('confirm')) && b.disabled;
|
|
1434
1491
|
});
|
|
1435
1492
|
if (disabledAllBtn) {
|
|
1493
|
+
// For harvest: disabled means nothing to harvest — skip to hoe.
|
|
1494
|
+
// For hoe/water/plant: disabled means we don't have the item — try to buy it.
|
|
1495
|
+
const ITEM_FOR_ACTION = { hoe: 'Hoe', water: 'Watering Can', plant: 'Seeds' };
|
|
1496
|
+
const missingItem = ITEM_FOR_ACTION[action];
|
|
1497
|
+
if (missingItem) {
|
|
1498
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled — trying to buy ${missingItem}`);
|
|
1499
|
+
const bought = await tryBuyFarmItem({ missing: missingItem, channel, waitForDankMemer, client });
|
|
1500
|
+
if (bought.ok) {
|
|
1501
|
+
LOG.success(`[farm:cycle:${cycleDepth}] Bought ${bought.itemName} — retrying ${action}`);
|
|
1502
|
+
await sleep(1200);
|
|
1503
|
+
// Restart the cycle from farm view to refresh button state
|
|
1504
|
+
await channel.send('pls farm view');
|
|
1505
|
+
const re = await waitForDankMemer(12000);
|
|
1506
|
+
if (!re) { LOG.warn('[farm:cycle] no response after buy retry'); break; }
|
|
1507
|
+
if (isCV2(re)) await ensureCV2(re);
|
|
1508
|
+
cycleResponse = re;
|
|
1509
|
+
text = getFullText(cycleResponse);
|
|
1510
|
+
clean = brief(text, 600);
|
|
1511
|
+
initialAnalysis = null;
|
|
1512
|
+
continue;
|
|
1513
|
+
} else {
|
|
1514
|
+
LOG.warn(`[farm:cycle:${cycleDepth}] Could not buy ${missingItem} — skipping to next phase`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1436
1517
|
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1437
1518
|
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1438
1519
|
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (no-op) — next=${nextPhase || 'done'}`);
|
|
@@ -1460,12 +1541,98 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1460
1541
|
// Step 5: advance past any confirmation screens back to the manage menu
|
|
1461
1542
|
cycleResponse = await advancePastConfirmation(lastApplyResp, waitForDankMemer);
|
|
1462
1543
|
if (!cycleResponse) { LOG.warn('[farm:cycle] confirmation advance returned null'); break; }
|
|
1544
|
+
if (isCV2(cycleResponse)) await ensureCV2(cycleResponse);
|
|
1463
1545
|
actionsTaken++;
|
|
1464
1546
|
lastAction = action;
|
|
1465
1547
|
text = getFullText(cycleResponse);
|
|
1466
1548
|
clean = brief(text, 600);
|
|
1467
1549
|
logFarmState('after-confirm', cycleResponse);
|
|
1468
1550
|
|
|
1551
|
+
// ── Farm text detection: determine next action from farm state ─────────────────
|
|
1552
|
+
// After confirmation advances, we may be on the farm view (no action tabs visible).
|
|
1553
|
+
// Use farm text patterns to determine what to do next — this is more reliable
|
|
1554
|
+
// than the button state check (which fails on the farm view).
|
|
1555
|
+
{
|
|
1556
|
+
const postBtns = getAllButtons(cycleResponse);
|
|
1557
|
+
const postActionBtns = postBtns.filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
1558
|
+
const postHasActionTabs = postActionBtns.some(b => hasAny(b, ['hoe', 'water', 'plant', 'harvest', 'fertiliz']));
|
|
1559
|
+
const postLower = clean.toLowerCase();
|
|
1560
|
+
const hasSeedsReady = /seeds ready|seeds.*ready|planted|crop.*ready/i.test(postLower);
|
|
1561
|
+
const hasHarvestReady = /ready to harvest|harvest ready|can be harvest|can harvest|wilt/i.test(postLower);
|
|
1562
|
+
const isEmpty = /pretty empty|seems empty|empty\.{0,3}/i.test(postLower) && !hasSeedsReady && !hasHarvestReady;
|
|
1563
|
+
if (!postHasActionTabs && !isEmpty) {
|
|
1564
|
+
// Farm has crops (planted/growing). Force harvest.
|
|
1565
|
+
// cycleResponse is still the farm view (no action tabs) after advancePastConfirmation.
|
|
1566
|
+
// Re-enter the manage menu so findNextFarmActionFromManage has buttons to work with.
|
|
1567
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1568
|
+
if (mb) {
|
|
1569
|
+
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: re-entering manage menu to force harvest`);
|
|
1570
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: 'farm-farmtext-harvest-manage' });
|
|
1571
|
+
if (mr) {
|
|
1572
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1573
|
+
cycleResponse = mr;
|
|
1574
|
+
text = getFullText(cycleResponse);
|
|
1575
|
+
clean = brief(text, 600);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
forcedNextAction = 'harvest';
|
|
1579
|
+
lastAction = null;
|
|
1580
|
+
cycleDepth++;
|
|
1581
|
+
initialAnalysis = null;
|
|
1582
|
+
await sleep(300);
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (!postHasActionTabs && isEmpty) {
|
|
1586
|
+
// Farm is empty after the action. Re-enter manage menu before forcing hoe.
|
|
1587
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1588
|
+
if (mb) {
|
|
1589
|
+
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: re-entering manage menu (empty farm)`);
|
|
1590
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: 'farm-farmtext-hoe-manage' });
|
|
1591
|
+
if (mr) {
|
|
1592
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1593
|
+
cycleResponse = mr;
|
|
1594
|
+
text = getFullText(cycleResponse);
|
|
1595
|
+
clean = brief(text, 600);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
forcedNextAction = 'hoe';
|
|
1599
|
+
lastAction = null;
|
|
1600
|
+
cycleDepth++;
|
|
1601
|
+
initialAnalysis = null;
|
|
1602
|
+
await sleep(300);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
// Otherwise: we're on manage menu or have action tabs — proceed to find next action.
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// ── Harvest ephemeral: parse "You harvested: Nothing" to detect no-op ──────────
|
|
1609
|
+
if (action === 'harvest') {
|
|
1610
|
+
const ephemeral = lastApplyResp?._capturedEphemeral;
|
|
1611
|
+
if (ephemeral) {
|
|
1612
|
+
const cv2t = ephemeral.cv2Text || '';
|
|
1613
|
+
const allt = ephemeral.allText || '';
|
|
1614
|
+
const harvestText = cv2t || allt;
|
|
1615
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] "${harvestText.slice(0, 300)}"`);
|
|
1616
|
+
// If nothing was harvested, cells are now empty (post-harvest debris).
|
|
1617
|
+
// Force the cycle to proceed to hoe rather than re-analyzing the same state.
|
|
1618
|
+
if (/nothing|0\s*x\s*\w|no\s+crops|\bnone\b/i.test(harvestText)) {
|
|
1619
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] Nothing harvested — forcing next action to hoe`);
|
|
1620
|
+
forcedNextAction = 'hoe';
|
|
1621
|
+
lastAction = 'harvest';
|
|
1622
|
+
cycleDepth++;
|
|
1623
|
+
initialAnalysis = null;
|
|
1624
|
+
await sleep(300);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
// Something was actually harvested — parse what and log it.
|
|
1628
|
+
const itemMatches = [...harvestText.matchAll(/-?\s*(\d+)\s*x?\s*([A-Za-z]+)/g)];
|
|
1629
|
+
if (itemMatches.length > 0) {
|
|
1630
|
+
const items = itemMatches.map(m => `${m[1]}x ${m[2]}`).join(', ');
|
|
1631
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] Harvested: ${items}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1469
1636
|
// If confirmation screen is identical to post-confirmation screen,
|
|
1470
1637
|
// check whether we're on a planted confirmation (Dank Memer planted crops
|
|
1471
1638
|
// and shows "ready at X"). In that case, click "Back" to return to the
|
|
@@ -1537,17 +1704,38 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1537
1704
|
const score = scoreResult?.score || null;
|
|
1538
1705
|
const nextAction = inferNextActionFromScore(action, score);
|
|
1539
1706
|
|
|
1707
|
+
// Collect all error/rejection text sources:
|
|
1708
|
+
// 1. Extra follow-up messages from farm actions
|
|
1709
|
+
// 2. CV2 interaction ACK (ephemeral or success/error response)
|
|
1710
|
+
// 3. The main message text after the action
|
|
1711
|
+
const ackContent = captureCvvAckContent(lastApplyResp?._capturedAck);
|
|
1712
|
+
const extraContent = lastApplyResp?._farmExtraMsg
|
|
1713
|
+
? String(stripAnsi(getFullText(lastApplyResp._farmExtraMsg) || '')).replace(/\s+/g, ' ').trim()
|
|
1714
|
+
: '';
|
|
1715
|
+
const postContent = String(stripAnsi(getFullText(lastApplyResp) || '')).replace(/\s+/g, ' ').trim();
|
|
1716
|
+
|
|
1717
|
+
if (ackContent || extraContent) {
|
|
1718
|
+
const flags = lastApplyResp?._capturedAck?.flags ?? 0;
|
|
1719
|
+
const isEphemeral = (flags & 64) !== 0;
|
|
1720
|
+
const level = isEphemeral ? LOG.warn : LOG.info;
|
|
1721
|
+
level(`[farm:cycle:${cycleDepth}:ephemeral] ephemeral=${isEphemeral} ack="${ackContent.slice(0, 300)}" extra="${extraContent.slice(0, 200)}"`);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Build combined error text for rejection detection
|
|
1725
|
+
const allErrorText = [extraContent, ackContent].filter(Boolean).join(' ');
|
|
1726
|
+
|
|
1540
1727
|
// Handle rejection messages that indicate wrong phase.
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1728
|
+
if (action === 'plant' && /only\s+plant\s+seeds\s+on\s+an\s+empty\s+tile|tilled\s+and\s+watered|can only plant seeds on an empty tile/.test(allErrorText)) {
|
|
1729
|
+
LOG.warn(`[farm:cycle] Plant rejected — need hoe+water first. Restarting from hoe. err="${allErrorText.slice(0, 200)}"`);
|
|
1730
|
+
forcedNextAction = 'hoe';
|
|
1544
1731
|
lastAction = null;
|
|
1545
1732
|
cycleDepth++;
|
|
1546
1733
|
await sleep(400);
|
|
1547
1734
|
continue;
|
|
1548
1735
|
}
|
|
1549
|
-
if (action === 'hoe' && /can only use.*hoe.*empty tile|after a harvest/.test(
|
|
1736
|
+
if (action === 'hoe' && /can only use.*hoe.*empty tile|after a harvest/.test(allErrorText)) {
|
|
1550
1737
|
LOG.warn('[farm:cycle] Hoe rejected — moving to water phase.');
|
|
1738
|
+
forcedNextAction = 'water';
|
|
1551
1739
|
lastAction = 'hoe';
|
|
1552
1740
|
cycleDepth++;
|
|
1553
1741
|
await sleep(400);
|
|
@@ -1556,6 +1744,22 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1556
1744
|
|
|
1557
1745
|
if (!nextAction) {
|
|
1558
1746
|
LOG.info(`[farm:cycle:${cycleDepth}] ${action} done — no next action (score=${score?.score ?? '?'}/${score?.threshold ?? '?'}, conf=${score?.confidence ?? '?'})`);
|
|
1747
|
+
// Enforce strict phase ordering: after water → harvest. Never route to plant from image analysis.
|
|
1748
|
+
if (action === 'water') {
|
|
1749
|
+
const harvestIdx = FARM_PHASE_ORDER.indexOf('harvest');
|
|
1750
|
+
if (harvestIdx >= 0) {
|
|
1751
|
+
const harvestBtn = managedActions.harvest || getAllButtons(cycleResponse).find(b => hasAny(b, ['harvest', 'reap', 'collect']));
|
|
1752
|
+
if (harvestBtn) {
|
|
1753
|
+
LOG.info(`[farm:cycle:${cycleDepth}] enforcing harvest after water (strict phase order)`);
|
|
1754
|
+
forcedNextAction = 'harvest';
|
|
1755
|
+
cycleDepth++;
|
|
1756
|
+
lastAction = 'water';
|
|
1757
|
+
initialAnalysis = null;
|
|
1758
|
+
await sleep(300);
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1559
1763
|
break;
|
|
1560
1764
|
}
|
|
1561
1765
|
|