dankgrinder 7.6.0 → 7.11.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/bin/dankgrinder.js +2 -2
- package/lib/commands/farm.js +416 -86
- package/lib/commands/farmVision.js +190 -96
- package/lib/commands/utils.js +17 -3
- package/lib/grinder.js +77 -20
- package/lib/rawLogger.js +23 -2
- package/package.json +1 -1
package/lib/commands/farm.js
CHANGED
|
@@ -4,15 +4,10 @@ const {
|
|
|
4
4
|
isCV2, ensureCV2, stripAnsi, needsItem, clickCV2SelectMenu,
|
|
5
5
|
} = require('./utils');
|
|
6
6
|
const { buyItem, buyItemsBatch } = require('./shop');
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
gridToString,
|
|
12
|
-
evaluateActionNeed,
|
|
13
|
-
evaluateActionScores,
|
|
14
|
-
dumpFarmVisionDebug,
|
|
15
|
-
} = require('./farmVision');
|
|
7
|
+
const rawLogger = require('../../lib/rawLogger');
|
|
8
|
+
// NOTE: Vision/ML image analysis is disabled — farm decisions are purely based on
|
|
9
|
+
// ephemeral responses and text detection. Re-enable by importing:
|
|
10
|
+
// const { downloadImage, extractFarmImageUrl, analyzeFarmGrid, gridToString } = require('./farmVision');
|
|
16
11
|
|
|
17
12
|
const RE_TS = /<t:(\d+):R>/;
|
|
18
13
|
const RE_MIN = /(\d+)\s*minute/i;
|
|
@@ -413,6 +408,7 @@ function logEphemeralLike(tag, payload) {
|
|
|
413
408
|
else LOG.info(line);
|
|
414
409
|
}
|
|
415
410
|
|
|
411
|
+
/* DISABLED — vision-based image analysis for action scoring
|
|
416
412
|
async function analyzeFarmImageForAction(msg, actionName) {
|
|
417
413
|
try {
|
|
418
414
|
const url = extractFarmImageUrl(msg);
|
|
@@ -485,6 +481,9 @@ function getVisionRepairPlan(actionName, score) {
|
|
|
485
481
|
return null;
|
|
486
482
|
}
|
|
487
483
|
|
|
484
|
+
/* DISABLED — vision-based action scoring (no longer used)
|
|
485
|
+
// ── Vision-based next-action inference ─────────────────────────────────────────
|
|
486
|
+
// Rely purely on ephemeral responses instead of ML image analysis.
|
|
488
487
|
function inferNextActionFromScore(actionName, score) {
|
|
489
488
|
if (!score) return null;
|
|
490
489
|
const planted = score.ratios?.planted || 0;
|
|
@@ -504,7 +503,9 @@ function inferNextActionFromScore(actionName, score) {
|
|
|
504
503
|
}
|
|
505
504
|
return null;
|
|
506
505
|
}
|
|
506
|
+
*/
|
|
507
507
|
|
|
508
|
+
/* DISABLED — vision-based preferred action inference
|
|
508
509
|
async function inferPreferredActionFromImage(msg) {
|
|
509
510
|
try {
|
|
510
511
|
const url = extractFarmImageUrl(msg);
|
|
@@ -517,43 +518,18 @@ async function inferPreferredActionFromImage(msg) {
|
|
|
517
518
|
planted: (analysis?.counts?.planted || 0) / slots,
|
|
518
519
|
unknown: (analysis?.counts?.unknown || 0) / slots,
|
|
519
520
|
};
|
|
520
|
-
|
|
521
521
|
let suggested = null;
|
|
522
|
-
// Image-first phase routing (3-state: tilled/planted/unknown, no separate wet state).
|
|
523
522
|
if (ratios.planted >= 0.30) suggested = 'plant';
|
|
524
523
|
else if (ratios.tilled >= 0.30) suggested = 'water';
|
|
525
524
|
else suggested = 'hoe';
|
|
526
|
-
|
|
527
|
-
let dbg = null;
|
|
528
|
-
try {
|
|
529
|
-
dbg = await dumpFarmVisionDebug({
|
|
530
|
-
imgBuffer: buf,
|
|
531
|
-
analysis,
|
|
532
|
-
actionName: `phase-${suggested || 'unknown'}`,
|
|
533
|
-
sourceUrl: url,
|
|
534
|
-
});
|
|
535
|
-
} catch (e) {
|
|
536
|
-
LOG.warn(`[farm:phase-image] debug dump failed: ${e.message}`);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} counts=${JSON.stringify(analysis.counts)} conf=${analysis.avgConfidence} suggested=${suggested} ratios=${JSON.stringify({
|
|
540
|
-
tilled: +ratios.tilled.toFixed(3),
|
|
541
|
-
planted: +ratios.planted.toFixed(3),
|
|
542
|
-
unknown: +ratios.unknown.toFixed(3),
|
|
543
|
-
})}`);
|
|
544
|
-
if (dbg) {
|
|
545
|
-
const tileSummary = (analysis.cells || [])
|
|
546
|
-
.map(c => `r${c.row + 1}c${c.col + 1}:${c.state}:${c.confidence}`)
|
|
547
|
-
.join(' | ');
|
|
548
|
-
LOG.info(`[farm:phase-image] debug dir=${dbg.dir} source=${dbg.sourcePath} manifest=${dbg.manifestPath} tiles=${dbg.tileCount}`);
|
|
549
|
-
LOG.info(`[farm:phase-image] tile-summary ${tileSummary}`);
|
|
550
|
-
}
|
|
525
|
+
LOG.info(`[farm:phase-image] url=${url} grid=${gridToString(analysis)} suggested=${suggested}`);
|
|
551
526
|
return suggested;
|
|
552
527
|
} catch (e) {
|
|
553
528
|
LOG.warn(`[farm:phase-image] inference failed: ${e.message}`);
|
|
554
529
|
return null;
|
|
555
530
|
}
|
|
556
531
|
}
|
|
532
|
+
*/
|
|
557
533
|
|
|
558
534
|
function chooseSeedOption(menu) {
|
|
559
535
|
const options = (menu?.options || []).filter(o => o && o.value);
|
|
@@ -641,11 +617,11 @@ async function ensurePlantSeedSelected({ response, waitForDankMemer, channel })
|
|
|
641
617
|
}
|
|
642
618
|
}
|
|
643
619
|
|
|
620
|
+
/* DISABLED — vision-based phase scoring
|
|
644
621
|
async function capturePhaseVisionScore({ msg, actionName, phaseTag }) {
|
|
645
622
|
if (!msg || !['hoe', 'water', 'plant', 'harvest'].includes(actionName)) {
|
|
646
623
|
return null;
|
|
647
624
|
}
|
|
648
|
-
|
|
649
625
|
const check = await analyzeFarmImageForAction(msg, actionName);
|
|
650
626
|
const s = check?.score;
|
|
651
627
|
if (s) {
|
|
@@ -655,6 +631,7 @@ async function capturePhaseVisionScore({ msg, actionName, phaseTag }) {
|
|
|
655
631
|
}
|
|
656
632
|
return check;
|
|
657
633
|
}
|
|
634
|
+
*/
|
|
658
635
|
|
|
659
636
|
function mergeBuyPlan(plan) {
|
|
660
637
|
const m = new Map();
|
|
@@ -938,6 +915,17 @@ function pickFarmActionButton(msg, text) {
|
|
|
938
915
|
return null;
|
|
939
916
|
}
|
|
940
917
|
|
|
918
|
+
// Captures the ephemeral interaction response content from a CV2 ack object.
|
|
919
|
+
// Dank Memer sends error/success text in the interaction ACK (type 4/7) content field.
|
|
920
|
+
function captureCvvAckContent(ack) {
|
|
921
|
+
if (!ack) return '';
|
|
922
|
+
const flags = ack?.flags ?? ack?.data?.flags ?? 0;
|
|
923
|
+
const content = String(stripAnsi(ack?.content || ack?.data?.content || '')).trim();
|
|
924
|
+
if (!content) return '';
|
|
925
|
+
const isEphemeral = (flags & 64) !== 0;
|
|
926
|
+
return `[ephemeral=${isEphemeral}] ${content}`;
|
|
927
|
+
}
|
|
928
|
+
|
|
941
929
|
async function clickAndCapture({ channel, waitForDankMemer, response, button, tag, timeoutMs = 9000 }) {
|
|
942
930
|
const baseline = brief(getFullText(response), 400);
|
|
943
931
|
const clickRes = await safeClickButton(response, button);
|
|
@@ -947,7 +935,30 @@ async function clickAndCapture({ channel, waitForDankMemer, response, button, ta
|
|
|
947
935
|
delete response._lastInteractionAck;
|
|
948
936
|
}
|
|
949
937
|
|
|
950
|
-
|
|
938
|
+
// CV2 HTTP fallback returns the interaction ACK (not the updated message).
|
|
939
|
+
// If clickRes looks like an ack rather than a message, extract it and wait for
|
|
940
|
+
// the edited message instead.
|
|
941
|
+
const isAckObject = clickRes && typeof clickRes === 'object' && !clickRes.id && (clickRes.interactionStatus || clickRes.interactionType);
|
|
942
|
+
const capturedAck = isAckObject ? clickRes : null;
|
|
943
|
+
|
|
944
|
+
// Register a one-time callback to capture ephemeral responses (e.g. "You harvested: Nothing")
|
|
945
|
+
// from the raw gateway packet — the only place where ephemeral embeds/CV2 text are preserved.
|
|
946
|
+
let capturedEphemeral = null;
|
|
947
|
+
rawLogger.onNextEphemeral((parsed) => {
|
|
948
|
+
capturedEphemeral = parsed;
|
|
949
|
+
const cv2t = parsed.cv2Text || '';
|
|
950
|
+
const allt = parsed.allText || '';
|
|
951
|
+
const txt = cv2t || allt;
|
|
952
|
+
if (txt.includes('harvested')) LOG.info(`${tag}-ephemeral harvest: ${txt.slice(0, 200)}`);
|
|
953
|
+
else LOG.info(`${tag}-ephemeral ${txt.slice(0, 200)}`);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// Extra grace period: the ephemeral often arrives after safeClickButton resolves
|
|
957
|
+
// (the library may return the stale message immediately, before the gateway delivers the
|
|
958
|
+
// ephemeral). This ensures we capture it even when it arrives late.
|
|
959
|
+
await sleep(1500);
|
|
960
|
+
|
|
961
|
+
let post = (!isAckObject && clickRes) ? clickRes : null;
|
|
951
962
|
if (!post && response.id) post = await waitForEditedMessage(channel, response.id, baseline, timeoutMs);
|
|
952
963
|
if (!post) post = await waitForDankMemer(timeoutMs);
|
|
953
964
|
|
|
@@ -956,17 +967,23 @@ async function clickAndCapture({ channel, waitForDankMemer, response, button, ta
|
|
|
956
967
|
|
|
957
968
|
// Capture any additional immediate callback message (often ephemeral-like
|
|
958
969
|
// "only you can see this" notices) that may be separate from edited CV2 post.
|
|
970
|
+
let extraMsg = null;
|
|
959
971
|
try {
|
|
960
972
|
const side = await waitForDankMemer(1500);
|
|
961
973
|
if (side && side.id !== post.id) {
|
|
962
974
|
logEphemeralLike(`${tag}-post-extra`, side);
|
|
963
|
-
|
|
975
|
+
extraMsg = side;
|
|
964
976
|
}
|
|
965
977
|
} catch {}
|
|
966
978
|
|
|
967
979
|
if (isCV2(post)) await ensureCV2(post);
|
|
968
980
|
logMsg(post, tag);
|
|
969
981
|
logFarmState(tag, post);
|
|
982
|
+
|
|
983
|
+
// Attach captured ack and extra message so callers can access them.
|
|
984
|
+
post._capturedAck = capturedAck;
|
|
985
|
+
post._farmExtraMsg = extraMsg;
|
|
986
|
+
post._capturedEphemeral = capturedEphemeral;
|
|
970
987
|
return post;
|
|
971
988
|
}
|
|
972
989
|
|
|
@@ -1074,10 +1091,22 @@ async function waitForEditedMessage(channel, messageId, baselineText, timeoutMs
|
|
|
1074
1091
|
// Determine the next action to take from the manage menu, given what the
|
|
1075
1092
|
// farm image vision says about the current state.
|
|
1076
1093
|
// Returns { action, button, reason } where action is null when the cycle is done.
|
|
1077
|
-
async function findNextFarmActionFromManage(msg, text, currentAction,
|
|
1094
|
+
async function findNextFarmActionFromManage(msg, text, currentAction, forcedNextAction) {
|
|
1078
1095
|
const btns = getAllButtons(msg).filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
1079
1096
|
const allBtns = getAllButtons(msg); // include disabled for state detection
|
|
1080
|
-
|
|
1097
|
+
// If no non-utility buttons remain, check if any action tabs exist.
|
|
1098
|
+
// This handles confirmation screens where Manage/Rename/Change Skin are all
|
|
1099
|
+
// filtered as nav buttons — the manage menu's action tabs (Hoe/Water/Plant/Harvest)
|
|
1100
|
+
// may still be accessible directly or via the manage button.
|
|
1101
|
+
if (btns.length === 0) {
|
|
1102
|
+
const actionTabs = allBtns.filter(b => !b.disabled && hasAny(b, ['hoe', 'water', 'plant', 'harvest', 'fertiliz']));
|
|
1103
|
+
if (actionTabs.length > 0) {
|
|
1104
|
+
// Action tabs exist but are buried behind a confirmation/nav screen.
|
|
1105
|
+
// The caller will need to navigate back to the manage menu.
|
|
1106
|
+
return { action: null, button: null, reason: 'confirmation-screen', actionTabs };
|
|
1107
|
+
}
|
|
1108
|
+
return { action: null, button: null, reason: 'no-buttons' };
|
|
1109
|
+
}
|
|
1081
1110
|
|
|
1082
1111
|
const managedActions = getManageActionButtons(msg);
|
|
1083
1112
|
const lower = String(stripAnsi(text || '')).toLowerCase();
|
|
@@ -1108,6 +1137,7 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1108
1137
|
if (btn) return { action: 'harvest', button: btn, reason: 'harvest-ready-text' };
|
|
1109
1138
|
}
|
|
1110
1139
|
|
|
1140
|
+
/* DISABLED — vision-based image analysis routing
|
|
1111
1141
|
// Use image vision ratios if available.
|
|
1112
1142
|
if (imageAnalysis) {
|
|
1113
1143
|
const slots = Math.max(1, (imageAnalysis.rows || 3) * (imageAnalysis.cols || 3));
|
|
@@ -1116,7 +1146,6 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1116
1146
|
planted: (imageAnalysis.counts?.planted || 0) / slots,
|
|
1117
1147
|
unknown: (imageAnalysis.counts?.unknown || 0) / slots,
|
|
1118
1148
|
};
|
|
1119
|
-
|
|
1120
1149
|
// Phase-aware selection: pick the earliest incomplete phase (3-state model).
|
|
1121
1150
|
// Harvest when planted >= 30%.
|
|
1122
1151
|
if (ratios.planted >= 0.30) {
|
|
@@ -1139,6 +1168,7 @@ async function findNextFarmActionFromManage(msg, text, currentAction, imageAnaly
|
|
|
1139
1168
|
if (btn) return { action: 'hoe', button: btn, reason: `hoe-vision(t=${ratios.tilled.toFixed(2)},p=${ratios.planted.toFixed(2)})` };
|
|
1140
1169
|
}
|
|
1141
1170
|
}
|
|
1171
|
+
*/
|
|
1142
1172
|
|
|
1143
1173
|
// Disabled-all detection: if the "All" for a phase is already disabled,
|
|
1144
1174
|
// that phase is done — move to the next one.
|
|
@@ -1172,11 +1202,20 @@ async function advancePastConfirmation(response, waitForDankMemer) {
|
|
|
1172
1202
|
if (!response) return null;
|
|
1173
1203
|
|
|
1174
1204
|
const CONFIRM_WORDS = ['continue', 'confirm', 'confirm all', 'done', 'back to farm', 'back', 'close'];
|
|
1205
|
+
// Also treat "All" buttons as confirmation actions (e.g. "Hoe All", "Water All").
|
|
1206
|
+
// Skip the plain "back" word to avoid matching the Back arrow button; prefer explicit
|
|
1207
|
+
// confirm words first, then fall back to "All" buttons as confirmation.
|
|
1208
|
+
const ALL_AS_CONFIRM = ['hoe all', 'water all', 'plant all', 'harvest all', 'fertilize all'];
|
|
1175
1209
|
const MAX_PAGES = 3;
|
|
1176
1210
|
|
|
1177
1211
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
1178
1212
|
const btns = getAllButtons(response).filter(b => !b.disabled);
|
|
1179
|
-
|
|
1213
|
+
let confirmBtn = btns.find(b => CONFIRM_WORDS.some(w => buttonHay(b).includes(w)));
|
|
1214
|
+
// Also treat "All" buttons as confirmation (e.g. clicking "Plant All" again on the
|
|
1215
|
+
// confirmation screen advances past it). But prefer explicit confirm words first.
|
|
1216
|
+
if (!confirmBtn) {
|
|
1217
|
+
confirmBtn = btns.find(b => ALL_AS_CONFIRM.some(w => buttonHay(b).includes(w)));
|
|
1218
|
+
}
|
|
1180
1219
|
|
|
1181
1220
|
if (!confirmBtn) {
|
|
1182
1221
|
// No more confirmation buttons — return the current screen (should be manage menu)
|
|
@@ -1188,10 +1227,28 @@ async function advancePastConfirmation(response, waitForDankMemer) {
|
|
|
1188
1227
|
await humanDelay(80, 220);
|
|
1189
1228
|
|
|
1190
1229
|
const baseline = brief(getFullText(response), 400);
|
|
1230
|
+
// NOTE: We do NOT register an ephemeral listener here.
|
|
1231
|
+
// The outer listener (registered by the caller) captures ALL ephemerals
|
|
1232
|
+
// including harvest results. Registering a second listener would clear
|
|
1233
|
+
// the listeners array after the first fires, preventing the outer
|
|
1234
|
+
// listener from seeing the ephemeral.
|
|
1235
|
+
|
|
1191
1236
|
let clicked;
|
|
1192
1237
|
try {
|
|
1193
1238
|
clicked = await safeClickButton(response, confirmBtn);
|
|
1194
1239
|
logEphemeralLike(`confirm-page-${page}`, clicked);
|
|
1240
|
+
// Preserve CV2 ack so cycle can check for ephemeral errors.
|
|
1241
|
+
if (clicked?._lastInteractionAck) {
|
|
1242
|
+
response._lastInteractionAck = clicked._lastInteractionAck;
|
|
1243
|
+
}
|
|
1244
|
+
// Also capture ephemeral ack content for logging
|
|
1245
|
+
const ackContent = captureCvvAckContent(clicked);
|
|
1246
|
+
if (ackContent) {
|
|
1247
|
+
const flags = clicked?.flags ?? 0;
|
|
1248
|
+
const isEphemeral = (flags & 64) !== 0;
|
|
1249
|
+
const level = isEphemeral ? LOG.warn : LOG.info;
|
|
1250
|
+
level(`[farm:confirm:${page}] ephemeral=${isEphemeral} ack="${ackContent.slice(0, 300)}"`);
|
|
1251
|
+
}
|
|
1195
1252
|
} catch (e) {
|
|
1196
1253
|
LOG.warn(`[farm:confirm] click failed on page ${page}: ${e.message}`);
|
|
1197
1254
|
return response;
|
|
@@ -1235,7 +1292,7 @@ async function advancePastConfirmation(response, waitForDankMemer) {
|
|
|
1235
1292
|
// ── Single-cycle farm orchestrator ──────────────────────────────────────────
|
|
1236
1293
|
// Sends `pls farm view` once, then completes the full hoe→water→plant→harvest
|
|
1237
1294
|
// cycle by looping on the returned manage menu — no additional command sends.
|
|
1238
|
-
async function runFarm({ channel, waitForDankMemer, client, redis, accountId }) {
|
|
1295
|
+
async function runFarm({ channel, waitForDankMemer, client, redis, accountId, forceRun }) {
|
|
1239
1296
|
LOG.cmd(`${c.white}${c.bold}pls farm view${c.reset}`);
|
|
1240
1297
|
|
|
1241
1298
|
await channel.send('pls farm view');
|
|
@@ -1292,11 +1349,21 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1292
1349
|
if (growReadySec > 0) LOG.info(`[farm] GROW-QUEUE DEBUG: lower=${lower.slice(0, 400)}`);
|
|
1293
1350
|
LOG.info(`[farm] grow-ready parse=${growReadySec == null ? 'none' : `${growReadySec}s`} redis_recovery=${redisRecoveryMs != null ? `${redisRecoveryMs}ms` : 'n/a'}`);
|
|
1294
1351
|
if (!redisRecovered && growReadySec && growReadySec > 20) {
|
|
1295
|
-
|
|
1296
|
-
//
|
|
1297
|
-
// This
|
|
1298
|
-
|
|
1299
|
-
|
|
1352
|
+
// Grow timer is active — schedule re-check based on how close crops are.
|
|
1353
|
+
// Graduated wait: short when nearly ready, longer when far out.
|
|
1354
|
+
// This avoids spamming Discord with farm commands when crops have hours left.
|
|
1355
|
+
let waitSec;
|
|
1356
|
+
if (growReadySec <= 300) {
|
|
1357
|
+
// < 5 min: check every minute (crops nearly ready)
|
|
1358
|
+
waitSec = 60;
|
|
1359
|
+
} else if (growReadySec <= 3600) {
|
|
1360
|
+
// 5-60 min: check every 5 minutes
|
|
1361
|
+
waitSec = 300;
|
|
1362
|
+
} else {
|
|
1363
|
+
// > 1 hour: check every 15 minutes (long grow, no rush)
|
|
1364
|
+
waitSec = 900;
|
|
1365
|
+
}
|
|
1366
|
+
LOG.info(`[farm] crops growing (~${Math.ceil(growReadySec / 60)}m remaining); re-checking in ${waitSec / 60}m`);
|
|
1300
1367
|
return { result: `farm grow queue (${Math.ceil(growReadySec / 60)}m)`, coins: 0, nextCooldownSec: waitSec, skipReason: 'farm_grow_queue' };
|
|
1301
1368
|
}
|
|
1302
1369
|
|
|
@@ -1333,19 +1400,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1333
1400
|
}
|
|
1334
1401
|
}
|
|
1335
1402
|
|
|
1336
|
-
// ── Initial image analysis to bootstrap cycle entry ────────────────────────
|
|
1337
|
-
let initialAnalysis = null;
|
|
1338
|
-
try {
|
|
1339
|
-
const url = extractFarmImageUrl(response);
|
|
1340
|
-
if (url) {
|
|
1341
|
-
const buf = await downloadImage(url);
|
|
1342
|
-
const grid = await analyzeFarmGrid(buf);
|
|
1343
|
-
initialAnalysis = grid;
|
|
1344
|
-
LOG.info(`[farm] initial image grid=${gridToString(grid)} counts=${JSON.stringify(grid.counts)} conf=${grid.avgConfidence}`);
|
|
1345
|
-
}
|
|
1346
|
-
} catch (e) {
|
|
1347
|
-
LOG.info(`[farm] initial image analysis failed: ${e.message}`);
|
|
1348
|
-
}
|
|
1349
1403
|
|
|
1350
1404
|
// ── Cycle loop: hoe → water → plant → harvest ─────────────────────────────
|
|
1351
1405
|
let cycleDepth = 0;
|
|
@@ -1354,11 +1408,14 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1354
1408
|
let actionsTaken = 0; // Track how many farm actions were actually executed
|
|
1355
1409
|
let forcedNextAction = null; // When advancing a phase, force the next action check
|
|
1356
1410
|
let lastApplyResp = null; // Track the last apply response for coin/cooldown parsing
|
|
1411
|
+
let lastRejectedAction = null; // Track last rejected action to break empty-farm loops
|
|
1357
1412
|
|
|
1358
1413
|
while (cycleDepth < 5) {
|
|
1414
|
+
// Reset per-cycle state
|
|
1415
|
+
let justRejected = false;
|
|
1359
1416
|
let actionResult;
|
|
1360
1417
|
try {
|
|
1361
|
-
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction,
|
|
1418
|
+
actionResult = await findNextFarmActionFromManage(cycleResponse, text, lastAction, forcedNextAction);
|
|
1362
1419
|
} catch (e) {
|
|
1363
1420
|
LOG.warn(`[farm:cycle] findNextFarmActionFromManage error: ${e.message}`);
|
|
1364
1421
|
break;
|
|
@@ -1367,6 +1424,23 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1367
1424
|
forcedNextAction = null;
|
|
1368
1425
|
|
|
1369
1426
|
if (!action || !button) {
|
|
1427
|
+
if (reason === 'confirmation-screen') {
|
|
1428
|
+
// We're on a confirmation/nav screen with no actionable buttons visible.
|
|
1429
|
+
// Re-enter the manage menu to get action tabs back.
|
|
1430
|
+
LOG.info(`[farm:cycle:${cycleDepth}] confirmation screen — re-entering manage menu`);
|
|
1431
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1432
|
+
if (mb) {
|
|
1433
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: `farm-cycle-confirm-manage` });
|
|
1434
|
+
if (mr) {
|
|
1435
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1436
|
+
cycleResponse = mr;
|
|
1437
|
+
text = getFullText(cycleResponse);
|
|
1438
|
+
clean = brief(text, 600);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
await sleep(300);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1370
1444
|
LOG.info(`[farm:cycle:${cycleDepth}] no action found (reason=${reason}) — breaking cycle`);
|
|
1371
1445
|
break;
|
|
1372
1446
|
}
|
|
@@ -1428,15 +1502,48 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1428
1502
|
label.includes('plant all') || label.includes('harvest all') || label.includes('confirm')) && b.disabled;
|
|
1429
1503
|
});
|
|
1430
1504
|
if (disabledAllBtn) {
|
|
1505
|
+
// Farm state check: "Hoe All" disabled could mean (a) no Hoe item OR (b) no tiles to work.
|
|
1506
|
+
// Check the farm text — if the farm is empty, there's nothing to hoe/water/plant,
|
|
1507
|
+
// so buying is pointless. Skip buy and treat as a no-op phase.
|
|
1508
|
+
const farmEmpty = /pretty empty|seems empty|empty\.{0,3}/i.test(clean);
|
|
1431
1509
|
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1432
1510
|
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1511
|
+
if (farmEmpty) {
|
|
1512
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled but farm is empty — skipping ${action}, advancing to ${nextPhase || 'done'}`);
|
|
1513
|
+
lastAction = action;
|
|
1514
|
+
forcedNextAction = nextPhase;
|
|
1515
|
+
cycleDepth++;
|
|
1516
|
+
if (!nextPhase) break;
|
|
1517
|
+
await sleep(300);
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
// Farm has tiles but all-btn disabled — try to buy the item.
|
|
1521
|
+
const ITEM_FOR_ACTION = { hoe: 'Hoe', water: 'Watering Can', plant: 'Seeds' };
|
|
1522
|
+
const missingItem = ITEM_FOR_ACTION[action];
|
|
1523
|
+
if (missingItem) {
|
|
1524
|
+
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (farm not empty) — trying to buy ${missingItem}`);
|
|
1525
|
+
const bought = await tryBuyFarmItem({ missing: missingItem, channel, waitForDankMemer, client });
|
|
1526
|
+
if (bought.ok) {
|
|
1527
|
+
LOG.success(`[farm:cycle:${cycleDepth}] Bought ${bought.itemName} — retrying ${action}`);
|
|
1528
|
+
await sleep(1200);
|
|
1529
|
+
// Refresh the manage menu to pick up the new button state
|
|
1530
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1531
|
+
if (mb) {
|
|
1532
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: 'farm-buy-retry-manage' });
|
|
1533
|
+
if (mr) { if (isCV2(mr)) await ensureCV2(mr); cycleResponse = mr; text = getFullText(cycleResponse); clean = brief(text, 600); }
|
|
1534
|
+
}
|
|
1535
|
+
continue;
|
|
1536
|
+
} else {
|
|
1537
|
+
LOG.warn(`[farm:cycle:${cycleDepth}] Could not buy ${missingItem} — skipping to next phase`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1433
1541
|
LOG.info(`[farm:cycle:${cycleDepth}] ${action} all-btn disabled (no-op) — next=${nextPhase || 'done'}`);
|
|
1434
1542
|
actionsTaken++;
|
|
1435
1543
|
lastAction = action;
|
|
1436
1544
|
forcedNextAction = nextPhase;
|
|
1437
1545
|
cycleDepth++;
|
|
1438
1546
|
if (!nextPhase) break;
|
|
1439
|
-
initialAnalysis = null;
|
|
1440
1547
|
await sleep(300);
|
|
1441
1548
|
continue;
|
|
1442
1549
|
}
|
|
@@ -1453,14 +1560,210 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1453
1560
|
if (!lastApplyResp) { LOG.warn('[farm:cycle] All click returned null'); break; }
|
|
1454
1561
|
|
|
1455
1562
|
// Step 5: advance past any confirmation screens back to the manage menu
|
|
1563
|
+
// Register the ephemeral listener BEFORE advancePastConfirmation so we capture
|
|
1564
|
+
// the confirmation ephemeral (e.g., harvest "You harvested:") which arrives
|
|
1565
|
+
// asynchronously after the HTTP response.
|
|
1566
|
+
let confirmEphemeral = null;
|
|
1567
|
+
rawLogger.onNextEphemeral((parsed) => {
|
|
1568
|
+
confirmEphemeral = parsed;
|
|
1569
|
+
});
|
|
1456
1570
|
cycleResponse = await advancePastConfirmation(lastApplyResp, waitForDankMemer);
|
|
1457
1571
|
if (!cycleResponse) { LOG.warn('[farm:cycle] confirmation advance returned null'); break; }
|
|
1572
|
+
if (isCV2(cycleResponse)) await ensureCV2(cycleResponse);
|
|
1573
|
+
// Ephemeral fires asynchronously after HTTP response — wait briefly for it to arrive
|
|
1574
|
+
// before the harvest block checks cycleResponse._capturedEphemeral.
|
|
1575
|
+
await sleep(200);
|
|
1576
|
+
if (confirmEphemeral) cycleResponse._capturedEphemeral = confirmEphemeral;
|
|
1577
|
+
else if (cycleResponse?._capturedEphemeral) { /* already set by advancePastConfirmation */ }
|
|
1458
1578
|
actionsTaken++;
|
|
1459
1579
|
lastAction = action;
|
|
1460
1580
|
text = getFullText(cycleResponse);
|
|
1461
1581
|
clean = brief(text, 600);
|
|
1462
1582
|
logFarmState('after-confirm', cycleResponse);
|
|
1463
1583
|
|
|
1584
|
+
// ── Rejection ephemeral: detect "wrong phase" errors and cascade ─────────────
|
|
1585
|
+
// advancePastConfirmation registers no ephemeral listener (confirmation ephemerals
|
|
1586
|
+
// are captured by the outer listener below). The action's ephemeral (from clicking
|
|
1587
|
+
// the action tab) remains on lastApplyResp._capturedEphemeral.
|
|
1588
|
+
{
|
|
1589
|
+
const ephem = lastApplyResp?._capturedEphemeral;
|
|
1590
|
+
const ephemText = (ephem?.cv2Text || ephem?.allText || '').toLowerCase();
|
|
1591
|
+
|
|
1592
|
+
// Helper: re-enter the manage menu from the farm view so the next
|
|
1593
|
+
// findNextFarmActionFromManage iteration has buttons to work with.
|
|
1594
|
+
async function reenterManage() {
|
|
1595
|
+
const allBtns = getAllButtons(cycleResponse);
|
|
1596
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1597
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] findFarmButton result: ${mb ? mb.label + ' / ' + mb.customId : 'NULL'}`);
|
|
1598
|
+
if (!mb) {
|
|
1599
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reenter] no Manage button found! available btns: ${allBtns.map(b => b.label).join(', ')}`);
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: `farm-cycle-reject-manage` });
|
|
1603
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] clickAndCapture result: ${mr ? 'got response' : 'NULL'}`);
|
|
1604
|
+
if (!mr) return false;
|
|
1605
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1606
|
+
cycleResponse = mr;
|
|
1607
|
+
const postBtns = getAllButtons(cycleResponse);
|
|
1608
|
+
LOG.info(`[farm:cycle:${cycleDepth}:reenter] post-manage btns: ${postBtns.length}, ${postBtns.map(b => b.label).join(', ')}`);
|
|
1609
|
+
return true;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// "You can only use a Hoe on empty tiles or after a harvest" → skip to water
|
|
1613
|
+
if (action === 'hoe' && /can only use.*hoe.*empty|only use.*hoe.*empty/.test(ephemText)) {
|
|
1614
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Hoe rejected: tiles not empty — cascading to water`);
|
|
1615
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1616
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1617
|
+
forcedNextAction = nextPhase;
|
|
1618
|
+
lastAction = action;
|
|
1619
|
+
justRejected = true;
|
|
1620
|
+
cycleDepth++;
|
|
1621
|
+
if (!nextPhase) break;
|
|
1622
|
+
await reenterManage();
|
|
1623
|
+
await sleep(300);
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// "You can only use a Watering Can on tilled tiles" (not hoed) → cascade forward to plant
|
|
1628
|
+
// Do NOT cascade back to hoe — that creates an infinite hoe↔water oscillation.
|
|
1629
|
+
if (action === 'water' && (ephemText.includes('can only use') || ephemText.includes('can only water'))) {
|
|
1630
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Water rejected (${ephemText.slice(0, 100)}) — cascading to plant`);
|
|
1631
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1632
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1633
|
+
forcedNextAction = nextPhase;
|
|
1634
|
+
lastAction = action;
|
|
1635
|
+
justRejected = true;
|
|
1636
|
+
// Break oscillation guard: if we bounced back here from plant, stop.
|
|
1637
|
+
if (lastRejectedAction === 'plant' && action === 'water') {
|
|
1638
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] hoe→water→plant oscillation detected — breaking`);
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
cycleDepth++;
|
|
1642
|
+
if (!nextPhase) break;
|
|
1643
|
+
await reenterManage();
|
|
1644
|
+
await sleep(300);
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// "You can only plant seeds on an empty tile that is tilled and watered" → cascade to harvest
|
|
1649
|
+
// Do NOT cascade back to hoe — that creates an infinite hoe↔water oscillation.
|
|
1650
|
+
if (action === 'plant' && /can only plant|can only use.*seed|plant.*not.*water/.test(ephemText)) {
|
|
1651
|
+
LOG.warn(`[farm:cycle:${cycleDepth}:reject] Plant rejected (${ephemText.slice(0, 100)}) — cascading to harvest`);
|
|
1652
|
+
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1653
|
+
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1654
|
+
forcedNextAction = nextPhase;
|
|
1655
|
+
lastAction = action;
|
|
1656
|
+
justRejected = true;
|
|
1657
|
+
cycleDepth++;
|
|
1658
|
+
if (!nextPhase) {
|
|
1659
|
+
// Plant is last phase — advance to harvest in next cycle.
|
|
1660
|
+
await sleep(300);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
await reenterManage();
|
|
1664
|
+
await sleep(300);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// ── Farm text detection: determine next action from farm state ─────────────────
|
|
1670
|
+
// Skip entirely if a rejection just fired — the rejection cascade handles the
|
|
1671
|
+
// next action; farm-text should not override forcedNextAction.
|
|
1672
|
+
if (!justRejected) {
|
|
1673
|
+
const postBtns = getAllButtons(cycleResponse);
|
|
1674
|
+
const postActionBtns = postBtns.filter(b => !b.disabled && !isNavOrUtilityButton(b));
|
|
1675
|
+
const postHasActionTabs = postActionBtns.some(b => hasAny(b, ['hoe', 'water', 'plant', 'harvest', 'fertiliz']));
|
|
1676
|
+
const postLower = clean.toLowerCase();
|
|
1677
|
+
const hasSeedsReady = /seeds ready|seeds.*ready|planted|crop.*ready/i.test(postLower);
|
|
1678
|
+
const hasHarvestReady = /ready to harvest|harvest ready|can be harvest|can harvest|wilt/i.test(postLower);
|
|
1679
|
+
const isEmpty = /pretty empty|seems empty|empty\.{0,3}/i.test(postLower) && !hasSeedsReady && !hasHarvestReady;
|
|
1680
|
+
if (!postHasActionTabs && !isEmpty) {
|
|
1681
|
+
// Farm has crops (planted/growing). Force harvest.
|
|
1682
|
+
// BUT: if the current action was 'harvest' and we're on a planted confirmation
|
|
1683
|
+
// screen (no action tabs, not empty), it means nothing was harvested.
|
|
1684
|
+
// Breaking here lets the grow queue handle the planted crops.
|
|
1685
|
+
const justHarvested = (action === 'harvest');
|
|
1686
|
+
if (justHarvested) {
|
|
1687
|
+
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: nothing harvested (planted confirmation) — breaking, grow queue will handle`);
|
|
1688
|
+
break;
|
|
1689
|
+
}
|
|
1690
|
+
// cycleResponse is still the farm view (no action tabs) after advancePastConfirmation.
|
|
1691
|
+
// Re-enter the manage menu so findNextFarmActionFromManage has buttons to work with.
|
|
1692
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1693
|
+
if (mb) {
|
|
1694
|
+
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: re-entering manage menu to force harvest`);
|
|
1695
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: 'farm-farmtext-harvest-manage' });
|
|
1696
|
+
if (mr) {
|
|
1697
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1698
|
+
cycleResponse = mr;
|
|
1699
|
+
text = getFullText(cycleResponse);
|
|
1700
|
+
clean = brief(text, 600);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
forcedNextAction = 'harvest';
|
|
1704
|
+
lastAction = null;
|
|
1705
|
+
cycleDepth++;
|
|
1706
|
+
await sleep(300);
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
if (!postHasActionTabs && isEmpty) {
|
|
1710
|
+
// Farm is empty after the action. Re-enter manage menu and check if there's
|
|
1711
|
+
// actually anything to work. If all "All" buttons are still disabled, the
|
|
1712
|
+
// farm is truly empty — nothing to hoe/water/plant, break the cycle.
|
|
1713
|
+
const mb = findFarmButton(cycleResponse, ['manage', 'farm-farm:manage']);
|
|
1714
|
+
if (mb) {
|
|
1715
|
+
LOG.info(`[farm:cycle:${cycleDepth}] farm-text: re-entering manage menu (empty farm)`);
|
|
1716
|
+
const mr = await clickAndCapture({ channel, waitForDankMemer, response: cycleResponse, button: mb, tag: 'farm-farmtext-hoe-manage' });
|
|
1717
|
+
if (mr) {
|
|
1718
|
+
if (isCV2(mr)) await ensureCV2(mr);
|
|
1719
|
+
cycleResponse = mr;
|
|
1720
|
+
text = getFullText(cycleResponse);
|
|
1721
|
+
clean = brief(text, 600);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
// Double-check: after re-entering manage, if hoe/water/plant all-btn are all still
|
|
1725
|
+
// disabled, the farm is truly empty — no tiles to work. Break the cycle.
|
|
1726
|
+
const allBtnsNow = getAllButtons(cycleResponse);
|
|
1727
|
+
const hoeAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('hoe all'));
|
|
1728
|
+
const waterAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('water all'));
|
|
1729
|
+
const plantAllEnabled = allBtnsNow.some(b => !b.disabled && buttonHay(b).includes('plant all'));
|
|
1730
|
+
if (!hoeAllEnabled && !waterAllEnabled && !plantAllEnabled) {
|
|
1731
|
+
LOG.info(`[farm:cycle:${cycleDepth}] all action all-btns still disabled after manage re-entry — farm is truly empty, breaking`);
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
forcedNextAction = 'hoe';
|
|
1735
|
+
lastAction = null;
|
|
1736
|
+
cycleDepth++;
|
|
1737
|
+
await sleep(300);
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
// Otherwise: we're on manage menu or have action tabs — proceed to find next action.
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// ── Harvest ephemeral: parse "You harvested: Nothing" to detect no-op ──────────
|
|
1744
|
+
if (action === 'harvest') {
|
|
1745
|
+
const ephemeral = cycleResponse?._capturedEphemeral || lastApplyResp?._capturedEphemeral;
|
|
1746
|
+
if (ephemeral) {
|
|
1747
|
+
const cv2t = ephemeral.cv2Text || '';
|
|
1748
|
+
const allt = ephemeral.allText || '';
|
|
1749
|
+
const harvestText = cv2t || allt;
|
|
1750
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] cv2Text="${cv2t.slice(0,200)}" allText="${allt.slice(0,200)}"`);
|
|
1751
|
+
// If nothing was harvested, cells are now empty (post-harvest debris).
|
|
1752
|
+
// Clear forcedNextAction and break — farm is done.
|
|
1753
|
+
if (/nothing|0\s*x\s*\w|no\s+crops|\bnone\b|^you harvested\s*$/im.test(harvestText) || (/\byou harvested\b/i.test(harvestText) && /\b(nothing|none)\b/i.test(harvestText))) {
|
|
1754
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] Nothing harvested — farm cycle complete`);
|
|
1755
|
+
forcedNextAction = null;
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
// Something was actually harvested — parse what and log it.
|
|
1759
|
+
const itemMatches = [...harvestText.matchAll(/-?\s*(\d+)\s*x?\s*([A-Za-z]+)/g)];
|
|
1760
|
+
if (itemMatches.length > 0) {
|
|
1761
|
+
const items = itemMatches.map(m => `${m[1]}x ${m[2]}`).join(', ');
|
|
1762
|
+
LOG.info(`[farm:cycle:${cycleDepth}:harvest-ephemeral] Harvested: ${items}`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1464
1767
|
// If confirmation screen is identical to post-confirmation screen,
|
|
1465
1768
|
// check whether we're on a planted confirmation (Dank Memer planted crops
|
|
1466
1769
|
// and shows "ready at X"). In that case, click "Back" to return to the
|
|
@@ -1488,6 +1791,9 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1488
1791
|
logFarmState('after-plant-back', cycleResponse);
|
|
1489
1792
|
}
|
|
1490
1793
|
// Plant succeeded — advance to harvest.
|
|
1794
|
+
// cycleResponse is now the farm view (after clicking Back).
|
|
1795
|
+
// Re-enter manage so findNextFarmActionFromManage has buttons on the next loop.
|
|
1796
|
+
await reenterManage();
|
|
1491
1797
|
const currentIdx = FARM_PHASE_ORDER.indexOf(action);
|
|
1492
1798
|
const nextPhase = FARM_PHASE_ORDER[currentIdx + 1] || null;
|
|
1493
1799
|
LOG.info(`[farm:cycle:${cycleDepth}] plant done, advancing to ${nextPhase || 'done'}`);
|
|
@@ -1495,7 +1801,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1495
1801
|
if (!nextPhase) break;
|
|
1496
1802
|
cycleDepth++;
|
|
1497
1803
|
lastAction = nextPhase;
|
|
1498
|
-
initialAnalysis = null;
|
|
1499
1804
|
await sleep(400);
|
|
1500
1805
|
continue;
|
|
1501
1806
|
}
|
|
@@ -1507,7 +1812,6 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1507
1812
|
if (!nextPhase) break;
|
|
1508
1813
|
cycleDepth++;
|
|
1509
1814
|
lastAction = nextPhase;
|
|
1510
|
-
initialAnalysis = null;
|
|
1511
1815
|
await sleep(300);
|
|
1512
1816
|
continue;
|
|
1513
1817
|
}
|
|
@@ -1515,34 +1819,44 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1515
1819
|
clean = brief(text, 600);
|
|
1516
1820
|
logFarmState('after-confirm', cycleResponse);
|
|
1517
1821
|
|
|
1518
|
-
// Step 6:
|
|
1519
|
-
let imgAnalysis = null;
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1822
|
+
// Step 6: NO image analysis — rely purely on ephemeral + text for next action decision
|
|
1823
|
+
// let imgAnalysis = null; // DISABLED vision
|
|
1824
|
+
// const scoreResult = await capturePhaseVisionScore({ msg: cycleResponse, actionName: action, phaseTag: `farm-cycle-${action}` });
|
|
1825
|
+
// const nextAction = inferNextActionFromScore(action, scoreResult?.score || null); // DISABLED
|
|
1826
|
+
const nextAction = null; // DISABLED — purely ephemeral-based flow
|
|
1827
|
+
|
|
1828
|
+
// Collect all error/rejection text sources:
|
|
1829
|
+
// 1. Extra follow-up messages from farm actions
|
|
1830
|
+
// 2. CV2 interaction ACK (ephemeral or success/error response)
|
|
1831
|
+
// 3. The main message text after the action
|
|
1832
|
+
const ackContent = captureCvvAckContent(lastApplyResp?._capturedAck);
|
|
1833
|
+
const extraContent = lastApplyResp?._farmExtraMsg
|
|
1834
|
+
? String(stripAnsi(getFullText(lastApplyResp._farmExtraMsg) || '')).replace(/\s+/g, ' ').trim()
|
|
1835
|
+
: '';
|
|
1836
|
+
const postContent = String(stripAnsi(getFullText(lastApplyResp) || '')).replace(/\s+/g, ' ').trim();
|
|
1837
|
+
|
|
1838
|
+
if (ackContent || extraContent) {
|
|
1839
|
+
const flags = lastApplyResp?._capturedAck?.flags ?? 0;
|
|
1840
|
+
const isEphemeral = (flags & 64) !== 0;
|
|
1841
|
+
const level = isEphemeral ? LOG.warn : LOG.info;
|
|
1842
|
+
level(`[farm:cycle:${cycleDepth}:ephemeral] ephemeral=${isEphemeral} ack="${ackContent.slice(0, 300)}" extra="${extraContent.slice(0, 200)}"`);
|
|
1529
1843
|
}
|
|
1530
1844
|
|
|
1531
|
-
|
|
1532
|
-
const
|
|
1533
|
-
const nextAction = inferNextActionFromScore(action, score);
|
|
1845
|
+
// Build combined error text for rejection detection
|
|
1846
|
+
const allErrorText = [extraContent, ackContent].filter(Boolean).join(' ');
|
|
1534
1847
|
|
|
1535
1848
|
// Handle rejection messages that indicate wrong phase.
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1849
|
+
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)) {
|
|
1850
|
+
LOG.warn(`[farm:cycle] Plant rejected — need hoe+water first. Restarting from hoe. err="${allErrorText.slice(0, 200)}"`);
|
|
1851
|
+
forcedNextAction = 'hoe';
|
|
1539
1852
|
lastAction = null;
|
|
1540
1853
|
cycleDepth++;
|
|
1541
1854
|
await sleep(400);
|
|
1542
1855
|
continue;
|
|
1543
1856
|
}
|
|
1544
|
-
if (action === 'hoe' && /can only use.*hoe.*empty tile|after a harvest/.test(
|
|
1857
|
+
if (action === 'hoe' && /can only use.*hoe.*empty tile|after a harvest/.test(allErrorText)) {
|
|
1545
1858
|
LOG.warn('[farm:cycle] Hoe rejected — moving to water phase.');
|
|
1859
|
+
forcedNextAction = 'water';
|
|
1546
1860
|
lastAction = 'hoe';
|
|
1547
1861
|
cycleDepth++;
|
|
1548
1862
|
await sleep(400);
|
|
@@ -1551,13 +1865,29 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1551
1865
|
|
|
1552
1866
|
if (!nextAction) {
|
|
1553
1867
|
LOG.info(`[farm:cycle:${cycleDepth}] ${action} done — no next action (score=${score?.score ?? '?'}/${score?.threshold ?? '?'}, conf=${score?.confidence ?? '?'})`);
|
|
1868
|
+
// Enforce strict phase ordering: after water → harvest. Never route to plant from image analysis.
|
|
1869
|
+
if (action === 'water') {
|
|
1870
|
+
const harvestIdx = FARM_PHASE_ORDER.indexOf('harvest');
|
|
1871
|
+
if (harvestIdx >= 0) {
|
|
1872
|
+
const harvestBtn = managedActions.harvest || getAllButtons(cycleResponse).find(b => hasAny(b, ['harvest', 'reap', 'collect']));
|
|
1873
|
+
if (harvestBtn) {
|
|
1874
|
+
LOG.info(`[farm:cycle:${cycleDepth}] enforcing harvest after water (strict phase order)`);
|
|
1875
|
+
forcedNextAction = 'harvest';
|
|
1876
|
+
cycleDepth++;
|
|
1877
|
+
lastAction = 'water';
|
|
1878
|
+
|
|
1879
|
+
await sleep(300);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1554
1884
|
break;
|
|
1555
1885
|
}
|
|
1556
1886
|
|
|
1557
1887
|
LOG.info(`[farm:cycle:${cycleDepth}] next action = ${nextAction} (after ${action})`);
|
|
1558
1888
|
lastAction = action;
|
|
1559
1889
|
cycleDepth++;
|
|
1560
|
-
|
|
1890
|
+
|
|
1561
1891
|
await sleep(300);
|
|
1562
1892
|
}
|
|
1563
1893
|
|
|
@@ -1597,7 +1927,7 @@ async function runFarm({ channel, waitForDankMemer, client, redis, accountId })
|
|
|
1597
1927
|
}
|
|
1598
1928
|
}
|
|
1599
1929
|
|
|
1600
|
-
|
|
1930
|
+
|
|
1601
1931
|
|
|
1602
1932
|
if (coins > 0) {
|
|
1603
1933
|
LOG.coin(`[farm] ${c.green}+⏣ ${coins.toLocaleString()}${c.reset}`);
|