feat: initial commit for TheFarmer project

This commit is contained in:
Karriis
2026-02-18 13:52:06 +08:00
commit 8ceb5fa9db
420 changed files with 61918 additions and 0 deletions

575
server/bot-worker.js Normal file
View File

@@ -0,0 +1,575 @@
const { createClient } = require('redis');
const { FarmBot } = require('./src/core/FarmBot');
const { loadProto, types } = require('./src/proto');
const { log, toLong, toNum } = require('./src/utils');
const { getLevelExpProgress, getPlantNameBySeedId } = require('./src/gameConfig');
const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
const redisPublisher = createClient({ url: redisUrl });
const redisSubscriber = createClient({ url: redisUrl });
const bots = new Map();
const locks = new Map();
function safeParseJson(input) {
try {
return JSON.parse(input);
} catch {
return {};
}
}
function publish(channel, payload) {
if (!redisPublisher.isOpen) return;
const body = JSON.stringify(payload || {});
redisPublisher.publish(channel, body).catch(() => {});
}
function getTodayKey() {
const now = new Date();
const y = now.getFullYear();
const m = `${now.getMonth() + 1}`.padStart(2, '0');
const d = `${now.getDate()}`.padStart(2, '0');
return `${y}-${m}-${d}`;
}
async function withEmailLock(email, task) {
if (!email) return task();
const prev = locks.get(email) || Promise.resolve();
let release;
const next = new Promise((resolve) => { release = resolve; });
locks.set(email, prev.then(() => next));
await prev;
try {
return await task();
} finally {
release();
if (locks.get(email) === next) locks.delete(email);
}
}
async function fetchTaskInfo(bot) {
const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish();
const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body);
const reply = types.TaskInfoReply.decode(replyBody);
return reply.task_info || null;
}
function buildTaskList(taskInfo) {
const tasks = [];
if (!taskInfo) return tasks;
const seen = new Set();
const seenContent = new Set();
const addTasks = (list, category) => {
if (!list) return;
for (const task of list) {
const id = toNum(task.id);
if (seen.has(id)) continue;
const progress = toNum(task.progress);
const totalProgress = toNum(task.total_progress);
const isClaimed = Boolean(task.is_claimed);
const isUnlocked = Boolean(task.is_unlocked);
const shareMultiple = toNum(task.share_multiple);
const rewards = [];
const rewardList = task.rewards || [];
for (const reward of rewardList) {
rewards.push({
id: toNum(reward.id),
count: toNum(reward.count)
});
}
const desc = task.desc || `任务#${id}`;
const contentKey = `${desc.trim()}|${progress}|${totalProgress}`;
if (seenContent.has(contentKey)) continue;
tasks.push({
id,
desc,
progress,
totalProgress,
isClaimed,
isUnlocked,
shareMultiple,
rewards,
category
});
seen.add(id);
seenContent.add(contentKey);
}
};
addTasks(taskInfo.growth_tasks, 'growth');
addTasks(taskInfo.daily_tasks, 'daily');
addTasks(taskInfo.tasks, 'normal');
return tasks;
}
function isTaskClaimable(task) {
if (!task.isUnlocked || task.isClaimed) return false;
if (task.totalProgress <= 0) return false;
return task.progress >= task.totalProgress;
}
async function claimTaskReward(bot, taskId, doShared) {
const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({
id: toLong(taskId),
do_shared: Boolean(doShared)
})).finish();
const { body: replyBody } = await bot.network.sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body);
return types.ClaimTaskRewardReply.decode(replyBody);
}
function attachBotEvents(email, bot) {
bot.on('started', () => {
publish(`bot-status-${email}`, { status: 'running' });
});
bot.on('stopped', () => {
publish(`bot-status-${email}`, { status: 'stopped' });
bots.delete(email);
});
bot.on('error', (err) => {
publish(`bot-error-${email}`, { message: err.message });
});
bot.on('log', (logData) => {
publish(`bot-log-${email}`, logData);
});
bot.on('stealRecord', (data) => {
publish(`bot-steal-${email}`, data || {});
});
}
async function buildStatus(bot) {
const user = bot.user || {};
const todayKey = getTodayKey();
const freeMallClaimed = user.freeMallClaimDate === todayKey;
const levelProgress = getLevelExpProgress(user.level || 0, user.exp || 0);
const tickets = user.tickets || 0;
const fertilizerContainer = Number(user.fertilizerContainer) || 0;
const fallbackHours = fertilizerContainer >= 3600 ? fertilizerContainer / 3600 : fertilizerContainer;
const organicFertilizerContainer = Number(user.organicFertilizerContainer) || 0;
const fallbackOrganicHours = organicFertilizerContainer >= 3600 ? organicFertilizerContainer / 3600 : organicFertilizerContainer;
let fertilizerHours = Number.isFinite(user.fertilizerHours) ? user.fertilizerHours : fallbackHours;
let organicFertilizerHours = Number.isFinite(user.organicFertilizerHours) ? user.organicFertilizerHours : fallbackOrganicHours;
if (bot.warehouseManager) {
try {
const bagReply = await bot.warehouseManager.getBag();
const items = bot.warehouseManager.getBagItems(bagReply);
let normalContainer = 0;
let organicContainer = 0;
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
if (id === 1011) normalContainer = count;
if (id === 1012) organicContainer = count;
}
const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer;
const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer;
const normalHours = normalContainerHours;
const organicHours = organicContainerHours;
fertilizerHours = normalHours;
user.normalFertilizerHours = normalHours;
user.organicFertilizerHours = organicHours;
organicFertilizerHours = organicHours;
} catch (error) {
log('状态', `获取背包失败: ${error.message}`);
}
}
return {
status: 'running',
user: {
nickname: user.name || 'Farmer',
level: user.level || 0,
exp: levelProgress.current || 0,
nextLevelExp: levelProgress.needed || 100,
gold: user.gold || 0,
tickets,
fertilizerHours,
normalFertilizerHours: Number.isFinite(user.normalFertilizerHours) ? user.normalFertilizerHours : fertilizerHours,
organicFertilizerHours,
avatarUrl: user.avatarUrl || '',
freeMallClaimed
}
};
}
function rpcOk(id, data) {
return { id, ok: true, data };
}
function rpcErr(id, error, status = 500) {
return { id, ok: false, error, status };
}
async function handleRpcRequest(req) {
const id = req && req.id;
const action = req && req.action;
const email = req && req.email;
const payload = (req && req.payload) || {};
if (!id || !action) return null;
try {
if (action === 'worker.ping') return rpcOk(id, { ok: true });
if (action === 'worker.stats') {
const memory = process.memoryUsage();
return rpcOk(id, {
activeBots: bots.size,
memory: {
rss: Math.round(memory.rss / 1024 / 1024),
heapTotal: Math.round(memory.heapTotal / 1024 / 1024),
heapUsed: Math.round(memory.heapUsed / 1024 / 1024)
},
uptime: Math.floor(process.uptime())
});
}
if (action === 'worker.stopAll') {
let stopped = 0;
for (const bot of bots.values()) {
try {
bot.stop();
stopped += 1;
} catch {
continue;
}
}
bots.clear();
return rpcOk(id, { success: true, stopped });
}
if (action === 'worker.shutdown') {
for (const bot of bots.values()) {
try {
bot.stop();
} catch {
continue;
}
}
bots.clear();
setTimeout(() => process.exit(0), 200);
return rpcOk(id, { success: true });
}
if (action === 'bot.isRunning') {
return rpcOk(id, { running: bots.has(email) });
}
if (action === 'bot.start') {
if (!email) return rpcErr(id, 'Email is required', 400);
const rawCode = typeof payload.code === 'string' ? payload.code.trim() : '';
payload.code = rawCode;
const code = rawCode;
if (!code) return rpcErr(id, 'Game code is required. Please provide it.', 400);
const response = await withEmailLock(email, async () => {
const existing = bots.get(email);
if (existing) {
existing.stop();
bots.delete(email);
}
const bot = new FarmBot(payload);
attachBotEvents(email, bot);
const started = await bot.start();
if (!started) return rpcErr(id, bot.lastStartError || '启动失败', 503);
bots.set(email, bot);
return rpcOk(id, { success: true });
});
return response;
}
if (action === 'bot.stop') {
if (!email) return rpcErr(id, 'Email is required', 400);
const response = await withEmailLock(email, async () => {
const bot = bots.get(email);
if (!bot) return rpcErr(id, 'Bot is not running', 404);
bot.stop();
bots.delete(email);
return rpcOk(id, { success: true });
});
return response;
}
if (action === 'bot.status') {
if (!email) return rpcErr(id, 'Email is required', 400);
const bot = bots.get(email);
if (!bot) return rpcOk(id, { status: 'stopped', user: null });
const data = await buildStatus(bot);
return rpcOk(id, data);
}
if (action === 'bot.updateConfig') {
if (!email) return rpcErr(id, 'Email is required', 400);
const response = await withEmailLock(email, async () => {
const bot = bots.get(email);
if (!bot) return rpcErr(id, 'Bot not running', 404);
const settings = payload && typeof payload === 'object'
? (payload.settings && typeof payload.settings === 'object' ? payload.settings : payload)
: {};
bot.applyConfig(settings);
return rpcOk(id, { success: true });
});
return response;
}
if (!email) return rpcErr(id, 'Email is required', 400);
const bot = bots.get(email);
if (!bot) return rpcErr(id, 'Bot not running', 404);
if (action === 'friends.list') {
if (bot.config && bot.config.enableFriendOps === false) {
return rpcErr(id, 'Friend operations disabled', 403);
}
const reply = await bot.friendManager.getAllFriends();
const friends = (reply.friends || []).map(f => ({
uin: f.uin ? f.uin.toString() : '',
userName: f.userName,
headPic: f.headPic,
yellowLevel: f.yellowLevel,
exp: f.exp,
money: f.money
}));
return rpcOk(id, { friends });
}
if (action === 'friends.visit') {
if (bot.config && bot.config.enableFriendOps === false) {
return rpcErr(id, 'Friend operations disabled', 403);
}
const friendUid = payload.friendUid;
const reply = await bot.friendManager.enterFriendFarm(friendUid);
const lands = reply && reply.farm && reply.farm.lands ? reply.farm.lands : [];
if (lands.length === 0) return rpcOk(id, { lands: [] });
const analysis = bot.friendManager.analyzeFriendLands(lands, bot.user.gid);
const formattedLands = lands.map(land => {
const landId = Number(land.id);
if (!land.unlocked) return { id: landId, type: 'locked' };
const plant = land.plant;
if (!plant || !plant.phases || plant.phases.length === 0) {
return { id: landId, type: 'empty' };
}
const currentPhase = bot.farmManager.getCurrentPhase(plant.phases, false);
const phaseVal = currentPhase ? currentPhase.phase : 0;
let status = 'growing';
if (phaseVal === 7) status = 'dead';
else if (phaseVal === 6) status = 'mature';
return {
id: landId,
type: 'planted',
status,
plantId: Number(plant.id),
plantName: getPlantNameBySeedId(Number(plant.id)) || `作物${plant.id}`,
phase: phaseVal,
needs: {
water: analysis.needWater.includes(landId),
weed: analysis.needWeed.includes(landId),
bug: analysis.needBug.includes(landId)
},
canSteal: analysis.stealable.includes(landId)
};
});
return rpcOk(id, { lands: formattedLands, farmUser: reply.farm.user });
}
if (action === 'friends.action') {
if (bot.config && bot.config.enableFriendOps === false) {
return rpcErr(id, 'Friend operations disabled', 403);
}
if (payload.actionType === 'steal' && bot.config && bot.config.enableSteal === false) {
return rpcErr(id, 'Steal disabled', 403);
}
let result;
let message = '';
if (payload.actionType === 'water') {
result = await bot.friendManager.helpWater(payload.friendUid, payload.landIds);
message = '浇水成功';
} else if (payload.actionType === 'weed') {
result = await bot.friendManager.helpWeed(payload.friendUid, payload.landIds);
message = '除草成功';
} else if (payload.actionType === 'insecticide') {
result = await bot.friendManager.helpInsecticide(payload.friendUid, payload.landIds);
message = '除虫成功';
} else if (payload.actionType === 'steal') {
result = await bot.friendManager.stealHarvest(payload.friendUid, payload.landIds);
message = '偷菜成功';
} else {
return rpcErr(id, 'Unknown action type', 400);
}
return rpcOk(id, { success: true, message, result });
}
if (action === 'warehouse.list') {
const data = await bot.warehouseManager.getFormattedBag();
return rpcOk(id, data);
}
if (action === 'warehouse.sell') {
const reply = await bot.warehouseManager.sellItems(payload.items);
const gold = bot.warehouseManager.extractGold(reply);
return rpcOk(id, { success: true, gold });
}
if (action === 'warehouse.use') {
const result = await bot.warehouseManager.useItems(payload.items);
return rpcOk(id, result);
}
if (action === 'tasks.list') {
const taskInfo = await fetchTaskInfo(bot);
const tasks = buildTaskList(taskInfo);
return rpcOk(id, { tasks });
}
if (action === 'tasks.claimAll') {
const taskInfo = await fetchTaskInfo(bot);
const tasks = buildTaskList(taskInfo);
const claimable = [];
for (const task of tasks) {
if (isTaskClaimable(task)) claimable.push(task);
}
let claimedCount = 0;
const failed = [];
for (const task of claimable) {
try {
await claimTaskReward(bot, task.id, task.shareMultiple > 1);
claimedCount += 1;
} catch (err) {
failed.push({ id: task.id, message: err.message });
}
}
const latestTaskInfo = await fetchTaskInfo(bot);
const latestTasks = buildTaskList(latestTaskInfo);
return rpcOk(id, { claimedCount, failed, tasks: latestTasks });
}
if (action === 'tasks.claimOne') {
const idNum = Number(payload.taskId);
if (!Number.isFinite(idNum) || idNum <= 0) return rpcErr(id, 'Invalid taskId', 400);
await claimTaskReward(bot, idNum, Boolean(payload.doShared));
const latestTaskInfo = await fetchTaskInfo(bot);
const latestTasks = buildTaskList(latestTaskInfo);
return rpcOk(id, { claimedCount: 1, failedCount: 0, tasks: latestTasks });
}
if (action === 'lands.list') {
const lands = await bot.farmManager.getFormattedLands();
return rpcOk(id, { lands });
}
if (action === 'lands.remove') {
const ids = Array.isArray(payload.landIds) ? payload.landIds.map(Number).filter(idNum => Number.isFinite(idNum) && idNum > 0) : [];
if (ids.length === 0) return rpcErr(id, 'Invalid landIds', 400);
await bot.farmManager.removePlant(ids);
return rpcOk(id, { success: true, removed: ids.length });
}
if (action === 'lands.unlock') {
const landId = Number(payload.landId);
if (!Number.isFinite(landId) || landId <= 0) return rpcErr(id, 'Invalid landId', 400);
const reply = await bot.farmManager.unlockLand(landId, Boolean(payload.doShared));
let land = reply && reply.land ? reply.land : null;
if (!land || !land.unlocked) {
const landsReply = await bot.farmManager.getAllLands();
const lands = landsReply && landsReply.lands ? landsReply.lands : [];
land = lands.find(item => Number(item.id) === landId) || null;
if (!land || !land.unlocked) {
return rpcErr(id, 'Land not unlocked', 409);
}
}
return rpcOk(id, { success: true, landId, land });
}
if (action === 'shop.list') {
const goods = await bot.shopManager.getSeedShopList();
return rpcOk(id, { goods });
}
if (action === 'shop.buy') {
await bot.shopManager.buyGoods(payload.goodsId, payload.count, payload.price);
return rpcOk(id, { success: true });
}
if (action === 'shop.paymallBuy') {
const itemId = Number(payload.itemId);
const buyCount = Number(payload.count ?? 1);
if (!Number.isFinite(itemId) || itemId <= 0) return rpcErr(id, 'Invalid itemId', 400);
if (!Number.isFinite(buyCount) || buyCount <= 0) return rpcErr(id, 'Invalid count', 400);
await bot.shopManager.purchaseMallItem(itemId, buyCount);
if (itemId === 1001) {
bot.user.freeMallClaimDate = getTodayKey();
bot.emit('userUpdate', bot.user);
}
return rpcOk(id, { success: true });
}
if (action === 'debug.fertilizer') {
const bagReply = await bot.warehouseManager.getBag();
const items = bot.warehouseManager.getBagItems(bagReply);
let normalContainer = 0;
let normal1 = 0;
let normal4 = 0;
let normal8 = 0;
let normal12 = 0;
let organicContainer = 0;
let organic1 = 0;
let organic4 = 0;
let organic8 = 0;
let organic12 = 0;
for (const item of items) {
const idNum = toNum(item.id);
const countNum = toNum(item.count);
if (idNum === 1011) normalContainer = countNum;
if (idNum === 80001) normal1 = countNum;
if (idNum === 80002) normal4 = countNum;
if (idNum === 80003) normal8 = countNum;
if (idNum === 80004) normal12 = countNum;
if (idNum === 1012) organicContainer = countNum;
if (idNum === 80011) organic1 = countNum;
if (idNum === 80012) organic4 = countNum;
if (idNum === 80013) organic8 = countNum;
if (idNum === 80014) organic12 = countNum;
}
const normalContainerHours = normalContainer >= 3600 ? normalContainer / 3600 : normalContainer;
const organicContainerHours = organicContainer >= 3600 ? organicContainer / 3600 : organicContainer;
const normalHours = normalContainerHours + normal1 + normal4 * 4 + normal8 * 8 + normal12 * 12;
const organicHours = organicContainerHours + organic1 + organic4 * 4 + organic8 * 8 + organic12 * 12;
return rpcOk(id, {
normal: {
container: normalContainer,
hour1: normal1,
hour4: normal4,
hour8: normal8,
hour12: normal12,
hours: normalHours
},
organic: {
container: organicContainer,
hour1: organic1,
hour4: organic4,
hour8: organic8,
hour12: organic12,
hours: organicHours
}
});
}
return rpcErr(id, 'Unknown action', 400);
} catch (err) {
const message = err && err.message ? err.message : 'Worker error';
return rpcErr(id, message, 500);
}
}
async function startWorker() {
await loadProto();
await redisPublisher.connect();
await redisSubscriber.connect();
await redisSubscriber.subscribe('bot:rpc:req', async (message) => {
const req = safeParseJson(message);
const response = await handleRpcRequest(req);
if (!response) return;
const body = JSON.stringify(response);
redisPublisher.publish('bot:rpc:res', body).catch(() => {});
});
log('Worker', 'Worker ready');
}
startWorker().catch((err) => {
log('Worker', `Worker failed: ${err.message}`);
process.exit(1);
});