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

139
211/server/client.js Normal file
View File

@@ -0,0 +1,139 @@
/**
* QQ经典农场 挂机脚本 - 入口文件
*
* 模块结构:
* src/config.js - 配置常量与枚举
* src/utils.js - 通用工具函数
* src/proto.js - Protobuf 加载与类型管理
* src/core/ - 核心逻辑 (FarmBot, Network, FarmManager, FriendManager)
* src/decode.js - PB解码/验证工具模式
*/
const { CONFIG } = require('./src/config');
const { loadProto } = require('./src/proto');
const { FarmBot } = require('./src/core/FarmBot');
const { verifyMode, decodeMode } = require('./src/decode');
const { emitRuntimeHint, sleep } = require('./src/utils');
// ============ 帮助信息 ============
function showHelp() {
console.log(`
QQ经典农场 挂机脚本 (重构版)
====================
用法:
node client.js --code <登录code> [--wx] [--interval <秒>] [--friend-interval <秒>]
node client.js --verify
node client.js --decode <数据> [--hex] [--gate] [--type <消息类型>]
参数:
--code 小程序 login() 返回的临时凭证 (必需)
--wx 使用微信登录 (默认为QQ小程序)
--interval 自己农场巡查完成后等待秒数, 默认10秒, 最低10秒
--friend-interval 好友巡查完成后等待秒数, 默认1秒, 最低1秒
--verify 验证proto定义
--decode 解码PB数据 (运行 --decode 无参数查看详细帮助)
功能:
- 自动收获成熟作物 → 购买种子 → 种植 → 施肥
- 自动除草、除虫、浇水
- 自动铲除枯死作物
- 自动巡查好友农场: 帮忙浇水/除草/除虫 + 偷菜
- 自动领取任务奖励 (支持分享翻倍)
- 每分钟自动出售仓库果实
- 启动时读取 share.txt 处理邀请码 (仅微信)
- 心跳保活
`);
}
// ============ 参数解析 ============
function parseArgs(args) {
const options = {
code: '',
deleteAccountMode: false,
name: '',
certId: '',
certType: 0,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--code' && args[i + 1]) {
options.code = args[++i];
}
if (args[i] === '--wx') {
CONFIG.platform = 'wx';
}
if (args[i] === '--interval' && args[i + 1]) {
const sec = parseInt(args[++i]);
CONFIG.farmCheckInterval = Math.max(sec, 1) * 1000;
}
if (args[i] === '--friend-interval' && args[i + 1]) {
const sec = parseInt(args[++i]);
CONFIG.friendCheckInterval = Math.max(sec, 1) * 1000; // 最低1秒
}
}
return options;
}
// ============ 主函数 ============
async function main() {
const args = process.argv.slice(2);
// 加载 proto 定义
await loadProto();
// 验证模式
if (args.includes('--verify')) {
await verifyMode();
return;
}
// 解码模式
if (args.includes('--decode')) {
await decodeMode(args);
return;
}
// 正常运行模式
const options = parseArgs(args);
if (!options.code) {
showHelp();
return;
}
// 更新 CONFIG 中的 code (注意: FarmBot 构造函数会合并 CONFIG 和传入的 config)
// 这里我们可以直接修改全局 CONFIG或者传入 options
// 为了兼容旧代码习惯,我们修改全局 CONFIG
// 但 FarmBot 建议传入 config
const botConfig = {
code: options.code,
// 其他参数已通过 modify global CONFIG 生效,或者也可以显式传入
};
console.log('正在启动 FarmBot...');
const bot = new FarmBot(botConfig);
// 优雅退出
process.on('SIGINT', () => {
console.log('\n正在停止...');
bot.stop();
process.exit(0);
});
try {
await bot.start();
// 保持进程运行 (如果 start() 返回后没有挂起的操作)
// FarmBot 内部启动了 interval所以进程应该会保持运行
} catch (error) {
console.error('Bot 运行出错:', error);
process.exit(1);
}
}
main().catch(err => {
console.error('未捕获的异常:', err);
process.exit(1);
});

BIN
211/server/data/users.db Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,802 @@
[
{
"level": 1,
"exp": 0
},
{
"level": 2,
"exp": 100
},
{
"level": 3,
"exp": 300
},
{
"level": 4,
"exp": 700
},
{
"level": 5,
"exp": 1300
},
{
"level": 6,
"exp": 2300
},
{
"level": 7,
"exp": 4000
},
{
"level": 8,
"exp": 6600
},
{
"level": 9,
"exp": 10100
},
{
"level": 10,
"exp": 14300
},
{
"level": 11,
"exp": 19300
},
{
"level": 12,
"exp": 25100
},
{
"level": 13,
"exp": 31800
},
{
"level": 14,
"exp": 39500
},
{
"level": 15,
"exp": 48300
},
{
"level": 16,
"exp": 58300
},
{
"level": 17,
"exp": 69500
},
{
"level": 18,
"exp": 82000
},
{
"level": 19,
"exp": 95900
},
{
"level": 20,
"exp": 111300
},
{
"level": 21,
"exp": 128300
},
{
"level": 22,
"exp": 146900
},
{
"level": 23,
"exp": 167200
},
{
"level": 24,
"exp": 189300
},
{
"level": 25,
"exp": 213300
},
{
"level": 26,
"exp": 239300
},
{
"level": 27,
"exp": 267300
},
{
"level": 28,
"exp": 297400
},
{
"level": 29,
"exp": 329700
},
{
"level": 30,
"exp": 364300
},
{
"level": 31,
"exp": 401300
},
{
"level": 32,
"exp": 440700
},
{
"level": 33,
"exp": 482600
},
{
"level": 34,
"exp": 527100
},
{
"level": 35,
"exp": 574300
},
{
"level": 36,
"exp": 624300
},
{
"level": 37,
"exp": 677100
},
{
"level": 38,
"exp": 732800
},
{
"level": 39,
"exp": 791500
},
{
"level": 40,
"exp": 853300
},
{
"level": 41,
"exp": 918300
},
{
"level": 42,
"exp": 986500
},
{
"level": 43,
"exp": 1058000
},
{
"level": 44,
"exp": 1132900
},
{
"level": 45,
"exp": 1211300
},
{
"level": 46,
"exp": 1293300
},
{
"level": 47,
"exp": 1378900
},
{
"level": 48,
"exp": 1468200
},
{
"level": 49,
"exp": 1561300
},
{
"level": 50,
"exp": 1658300
},
{
"level": 51,
"exp": 1759300
},
{
"level": 52,
"exp": 1864300
},
{
"level": 53,
"exp": 1973400
},
{
"level": 54,
"exp": 2086700
},
{
"level": 55,
"exp": 2204300
},
{
"level": 56,
"exp": 2326300
},
{
"level": 57,
"exp": 2452700
},
{
"level": 58,
"exp": 2583600
},
{
"level": 59,
"exp": 2719100
},
{
"level": 60,
"exp": 2859300
},
{
"level": 61,
"exp": 3004300
},
{
"level": 62,
"exp": 3154100
},
{
"level": 63,
"exp": 3308800
},
{
"level": 64,
"exp": 3468500
},
{
"level": 65,
"exp": 3633300
},
{
"level": 66,
"exp": 3803300
},
{
"level": 67,
"exp": 3978500
},
{
"level": 68,
"exp": 4159000
},
{
"level": 69,
"exp": 4344900
},
{
"level": 70,
"exp": 4536300
},
{
"level": 71,
"exp": 4733300
},
{
"level": 72,
"exp": 4935900
},
{
"level": 73,
"exp": 5144200
},
{
"level": 74,
"exp": 5358300
},
{
"level": 75,
"exp": 5578300
},
{
"level": 76,
"exp": 5804300
},
{
"level": 77,
"exp": 6036300
},
{
"level": 78,
"exp": 6274400
},
{
"level": 79,
"exp": 6518700
},
{
"level": 80,
"exp": 6769300
},
{
"level": 81,
"exp": 7026300
},
{
"level": 82,
"exp": 7289700
},
{
"level": 83,
"exp": 7559600
},
{
"level": 84,
"exp": 7836100
},
{
"level": 85,
"exp": 8119300
},
{
"level": 86,
"exp": 8409300
},
{
"level": 87,
"exp": 8706100
},
{
"level": 88,
"exp": 9009800
},
{
"level": 89,
"exp": 9320500
},
{
"level": 90,
"exp": 9638300
},
{
"level": 91,
"exp": 9963300
},
{
"level": 92,
"exp": 10295500
},
{
"level": 93,
"exp": 10635000
},
{
"level": 94,
"exp": 10981900
},
{
"level": 95,
"exp": 11336300
},
{
"level": 96,
"exp": 11698300
},
{
"level": 97,
"exp": 12067900
},
{
"level": 98,
"exp": 12445200
},
{
"level": 99,
"exp": 12830300
},
{
"level": 100,
"exp": 13223300
},
{
"level": 101,
"exp": 13624300
},
{
"level": 102,
"exp": 14185200
},
{
"level": 103,
"exp": 14760100
},
{
"level": 104,
"exp": 15349200
},
{
"level": 105,
"exp": 15952700
},
{
"level": 106,
"exp": 16570700
},
{
"level": 107,
"exp": 17203500
},
{
"level": 108,
"exp": 17851200
},
{
"level": 109,
"exp": 18513900
},
{
"level": 110,
"exp": 19191900
},
{
"level": 111,
"exp": 19885400
},
{
"level": 112,
"exp": 20594500
},
{
"level": 113,
"exp": 21319400
},
{
"level": 114,
"exp": 22060300
},
{
"level": 115,
"exp": 22817400
},
{
"level": 116,
"exp": 23590900
},
{
"level": 117,
"exp": 24381000
},
{
"level": 118,
"exp": 25187800
},
{
"level": 119,
"exp": 26011600
},
{
"level": 120,
"exp": 26852500
},
{
"level": 121,
"exp": 27710700
},
{
"level": 122,
"exp": 28586400
},
{
"level": 123,
"exp": 29479800
},
{
"level": 124,
"exp": 30391100
},
{
"level": 125,
"exp": 31320500
},
{
"level": 126,
"exp": 32268100
},
{
"level": 127,
"exp": 33234200
},
{
"level": 128,
"exp": 34218900
},
{
"level": 129,
"exp": 35222400
},
{
"level": 130,
"exp": 36245000
},
{
"level": 131,
"exp": 37286800
},
{
"level": 132,
"exp": 38348000
},
{
"level": 133,
"exp": 39428800
},
{
"level": 134,
"exp": 40529400
},
{
"level": 135,
"exp": 41650000
},
{
"level": 136,
"exp": 42790800
},
{
"level": 137,
"exp": 43952000
},
{
"level": 138,
"exp": 45133800
},
{
"level": 139,
"exp": 46336400
},
{
"level": 140,
"exp": 47559900
},
{
"level": 141,
"exp": 48804600
},
{
"level": 142,
"exp": 50070700
},
{
"level": 143,
"exp": 51358300
},
{
"level": 144,
"exp": 52667700
},
{
"level": 145,
"exp": 53999100
},
{
"level": 146,
"exp": 55352600
},
{
"level": 147,
"exp": 56728500
},
{
"level": 148,
"exp": 58127000
},
{
"level": 149,
"exp": 59548200
},
{
"level": 150,
"exp": 60992400
},
{
"level": 151,
"exp": 62459800
},
{
"level": 152,
"exp": 63950500
},
{
"level": 153,
"exp": 65464800
},
{
"level": 154,
"exp": 67002900
},
{
"level": 155,
"exp": 68564900
},
{
"level": 156,
"exp": 70151100
},
{
"level": 157,
"exp": 71761700
},
{
"level": 158,
"exp": 73396900
},
{
"level": 159,
"exp": 75056900
},
{
"level": 160,
"exp": 76741900
},
{
"level": 161,
"exp": 78452100
},
{
"level": 162,
"exp": 80187700
},
{
"level": 163,
"exp": 81948900
},
{
"level": 164,
"exp": 83735900
},
{
"level": 165,
"exp": 85548900
},
{
"level": 166,
"exp": 87388200
},
{
"level": 167,
"exp": 89253900
},
{
"level": 168,
"exp": 91146300
},
{
"level": 169,
"exp": 93065500
},
{
"level": 170,
"exp": 95011800
},
{
"level": 171,
"exp": 96985300
},
{
"level": 172,
"exp": 98986300
},
{
"level": 173,
"exp": 101015000
},
{
"level": 174,
"exp": 103071600
},
{
"level": 175,
"exp": 105156300
},
{
"level": 176,
"exp": 107269400
},
{
"level": 177,
"exp": 109411000
},
{
"level": 178,
"exp": 111581300
},
{
"level": 179,
"exp": 113780600
},
{
"level": 180,
"exp": 116009100
},
{
"level": 181,
"exp": 118267000
},
{
"level": 182,
"exp": 120554500
},
{
"level": 183,
"exp": 122871800
},
{
"level": 184,
"exp": 125219100
},
{
"level": 185,
"exp": 127596600
},
{
"level": 186,
"exp": 130004600
},
{
"level": 187,
"exp": 132443200
},
{
"level": 188,
"exp": 134912700
},
{
"level": 189,
"exp": 137413300
},
{
"level": 190,
"exp": 139945200
},
{
"level": 191,
"exp": 142508700
},
{
"level": 192,
"exp": 145103900
},
{
"level": 193,
"exp": 147731100
},
{
"level": 194,
"exp": 150390400
},
{
"level": 195,
"exp": 153082100
},
{
"level": 196,
"exp": 155806500
},
{
"level": 197,
"exp": 158563700
},
{
"level": 198,
"exp": 161353900
},
{
"level": 199,
"exp": 164177400
},
{
"level": 200,
"exp": 167034400
}
]

1090
211/server/index.js Normal file

File diff suppressed because it is too large Load Diff

3343
211/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
211/server/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "farm",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.13.5",
"body-parser": "^2.2.2",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^4.18.2",
"express-session": "^1.19.0",
"jsonwebtoken": "^9.0.3",
"long": "^5.3.2",
"mysql2": "^3.17.1",
"node-fetch": "^3.3.2",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"protobufjs": "^8.0.0",
"socket.io": "^4.8.3",
"sqlite3": "^5.1.7",
"ws": "^8.19.0"
}
}

View File

@@ -0,0 +1,27 @@
syntax = "proto3";
package corepb;
// ============ 通用物品 ============
message Item {
int64 id = 1; // 物品ID
int64 count = 2; // 数量
int64 expire_time = 3; // 过期时间
// field 4 reserved
// google.protobuf.Any detail = 5; // 详情 (略)
int64 uid = 6; // UID
bool is_new = 7; // 是否新获得
repeated int64 mutant_types = 8; // 变异类型
// ItemShow show = 100; // 展示信息 (略)
}
// 背包(物品列表)
message ItemBag {
repeated Item items = 1;
}
// ============ 物品变化 ============
message ItemChg {
Item item = 1; // 物品信息
int64 delta = 2; // 变化量 (正数增加, 负数减少)
}

View File

@@ -0,0 +1,120 @@
syntax = "proto3";
package gamepb.friendpb;
// ============ 好友农场摘要信息 ============
message Plant {
int64 dry_time_sec = 1; // 缺水倒计时秒
int64 weed_time_sec = 2; // 长草倒计时秒
int64 insect_time_sec = 3; // 生虫倒计时秒
int64 ripe_time_sec = 4; // 成熟倒计时秒
int64 ripe_fruit_id = 5; // 成熟果实ID
int64 steal_plant_num = 6; // 可偷数量
int64 dry_num = 7; // 缺水地块数
int64 weed_num = 8; // 有草地块数
int64 insect_num = 9; // 有虫地块数
}
// ============ 标签 ============
message Tags {
bool is_new = 1;
bool is_follow = 2;
}
// ============ 好友信息 ============
message GameFriend {
int64 gid = 1;
string open_id = 2;
string name = 3;
string avatar_url = 4;
string remark = 5;
int64 level = 6;
int64 gold = 7;
Tags tags = 8;
Plant plant = 9;
int32 authorized_status = 10;
// field 11 reserved
// Illustrated illustrated = 12; // 略
// repeated AvatarFrame equip_avatar_frames = 13; // 略
}
// ============ 请求/回复 ============
// --- 获取所有好友 ---
message GetAllRequest {}
message GetAllReply {
repeated GameFriend game_friends = 1;
// repeated Invitation invitations = 2; // 略
int64 application_count = 3;
}
// --- 同步好友 (带 open_ids) ---
message SyncAllRequest {
// field 1 reserved
repeated string open_ids = 2;
}
message SyncAllReply {
repeated GameFriend game_friends = 1;
// repeated Invitation invitations = 2; // 略
int64 application_count = 3;
}
// ============ 好友申请 (微信同玩) ============
// 申请信息
message Application {
int64 gid = 1;
int64 time_at = 2;
string open_id = 3;
string name = 4;
string avatar_url = 5;
int64 level = 6;
// repeated AvatarFrame equip_avatar_frames = 7; // 略
}
// --- 获取好友申请列表 ---
message GetApplicationsRequest {}
message GetApplicationsReply {
repeated Application applications = 1;
bool block_applications = 2; // 是否屏蔽申请
}
// --- 同意好友申请 ---
message AcceptFriendsRequest {
repeated int64 friend_gids = 1;
}
message AcceptFriendsReply {
repeated GameFriend friends = 1;
}
// --- 拒绝好友申请 ---
message RejectFriendsRequest {
repeated int64 friend_gids = 1;
}
message RejectFriendsReply {}
// --- 设置屏蔽申请 ---
message SetBlockApplicationsRequest {
bool block = 1;
}
message SetBlockApplicationsReply {
bool block = 1;
}
// ============ 服务器推送通知 ============
// 收到好友申请通知
message FriendApplicationReceivedNotify {
repeated Application applications = 1;
}
// 好友添加成功通知 (对方同意后)
message FriendAddedNotify {
repeated GameFriend friends = 1;
}

View File

@@ -0,0 +1,44 @@
syntax = "proto3";
// ============ gatepb ============
// 网关层协议 - 所有WS消息的外壳
package gatepb;
// Meta.Type 枚举
enum MessageType {
None = 0;
Request = 1;
Response = 2;
Notify = 3;
}
// 消息元信息
message Meta {
string service_name = 1;
string method_name = 2;
int32 message_type = 3; // MessageType enum
int64 client_seq = 4;
int64 server_seq = 5;
int64 error_code = 6;
string error_message = 7;
map<string, bytes> metadata = 8;
}
// 每个WS帧的结构
message Message {
Meta meta = 1;
bytes body = 2;
}
// 服务器推送事件
message EventMessage {
string message_type = 1;
bytes body = 2;
}
// 被踢下线通知
message KickoutNotify {
int64 reason = 1;
string reason_message = 2;
}

View File

@@ -0,0 +1,53 @@
syntax = "proto3";
package gamepb.itempb;
import "corepb.proto";
// ============ 背包/仓库 ============
// 获取背包
message BagRequest {}
message BagReply {
corepb.ItemBag item_bag = 1; // 与 game 一致,背包数据在 item_bag 里
}
// ============ 出售物品 ============
message SellRequest {
repeated corepb.Item items = 1; // 要出售的物品列表 (id + count)
}
message SellReply {
repeated corepb.Item sell_items = 1; // 出售的物品
repeated corepb.Item get_items = 2; // 获得的物品id=1001为金币, id=1002为点券
}
// ============ 使用物品 ============
message UseRequest {
int64 item_id = 1;
int64 count = 2;
repeated int64 land_ids = 3;
}
message UseReply {
repeated corepb.Item items = 1;
}
// ============ 批量使用物品 ============
message UseItem {
int64 item_id = 1;
int64 count = 2;
int64 land_count = 6; // 根据抓包字段6为18推测为土地数
}
message BatchUseRequest {
repeated UseItem items = 1;
}
message BatchUseReply {
repeated corepb.Item items = 1;
}

View File

@@ -0,0 +1,10 @@
syntax = "proto3";
package gamepb.itempb;
import "corepb.proto";
// ============ 物品变化通知 ============
message ItemNotify {
repeated corepb.ItemChg items = 1;
}

View File

@@ -0,0 +1,266 @@
syntax = "proto3";
package gamepb.plantpb;
// ============ 生长阶段枚举 ============
enum PlantPhase {
PHASE_UNKNOWN = 0;
SEED = 1; // 种子
GERMINATION = 2; // 发芽
SMALL_LEAVES = 3; // 小叶
LARGE_LEAVES = 4; // 大叶
BLOOMING = 5; // 开花
MATURE = 6; // 成熟 (可收获)
DEAD = 7; // 枯死
}
// ============ 土地信息 ============
message LandInfo {
int64 id = 1;
bool unlocked = 2;
int64 level = 3;
int64 max_level = 4;
bool could_unlock = 5;
bool could_upgrade = 6;
LandUnlockCondition unlock_condition = 7;
LandUpgradeCondition upgrade_condition = 8;
Buff buff = 9;
PlantInfo plant = 10;
bool is_shared = 11;
bool can_share = 12;
int64 master_land_id = 13;
repeated int64 slave_land_ids = 14;
int64 land_size = 15;
int64 lands_level = 16;
// 土地buff
message Buff {
int64 plant_yield_bonus = 1;
int64 planting_time_reduction = 2;
int64 plant_exp_bonus = 3;
}
}
// 土地解锁条件 (简化)
message LandUnlockCondition {
int64 need_level = 1;
int64 need_gold = 2;
// ... 其他条件字段
}
// 土地升级条件 (简化)
message LandUpgradeCondition {
int64 need_level = 1;
int64 need_gold = 2;
// ... 其他条件字段
}
// ============ 植物信息 ============
message PlantInfo {
int64 id = 1; // 植物/种子ID
string name = 2; // 名称
// field 3 reserved
repeated PlantPhaseInfo phases = 4; // 生长阶段列表
int64 season = 5; // 季节
int64 dry_num = 6; // 缺水次数 (>0需要浇水)
// fields 7,8 reserved
int64 stole_num = 9; // 被偷次数
int64 fruit_id = 10; // 果实ID
int64 fruit_num = 11; // 果实数量
repeated int64 weed_owners = 12; // 放草的人 (非空=有草)
repeated int64 insect_owners = 13; // 放虫的人 (非空=有虫)
repeated int64 stealers = 14; // 偷菜的人
int64 grow_sec = 15; // 生长秒数
bool stealable = 16; // 是否可偷
int64 left_inorc_fert_times = 17; // 剩余施肥次数
int64 left_fruit_num = 18; // 剩余果实数
int64 steal_intimacy_level = 19; // 偷菜亲密度等级
repeated int64 mutant_config_ids = 20; // 变异配置ID
bool is_nudged = 21; // 是否被催熟
}
// ============ 生长阶段详情 ============
message PlantPhaseInfo {
int32 phase = 1; // PlantPhase 枚举值
int64 begin_time = 2; // 阶段开始时间
int64 phase_id = 3; // 阶段ID
// fields 4,5 reserved
int64 dry_time = 6; // 变干时间 (>0 需要浇水)
int64 weeds_time = 7; // 长草时间 (>0 有杂草)
int64 insect_time = 8; // 生虫时间 (>0 有虫害)
map<int64, int64> ferts_used = 9; // 已用肥料
repeated MutantInfo mutants = 10; // 变异信息
}
// 变异信息
message MutantInfo {
int64 mutant_time = 1;
int64 mutant_config_id = 2;
int64 weather_id = 3;
}
// ============ 操作限制 ============
// 每种操作的每日限制信息
message OperationLimit {
int64 id = 1; // 操作类型ID
int64 day_times = 2; // 今日已操作次数
int64 day_times_lt = 3; // 每日操作上限
int64 day_share_id = 4; // 分享ID
int64 day_exp_times = 5; // 今日已获得经验次数
int64 day_ex_times_lt = 6; // 每日可获得经验上限
int64 day_exp_share_id = 7; // 经验分享ID
}
// 操作类型ID:
// 10001 = 帮好友浇水
// 10002 = 帮好友除虫
// 10003 = 帮好友除草
// 10004 = 偷菜
// 10005 = 放虫
// 10006 = 放草
// ============ 请求/回复消息 ============
// --- 获取所有土地 ---
message AllLandsRequest {
int64 host_gid = 1; // 0或自己的GID=查看自己
}
message AllLandsReply {
repeated LandInfo lands = 1;
repeated OperationLimit operation_limits = 2;
}
// --- 收获 ---
message HarvestRequest {
repeated int64 land_ids = 1; // 要收获的土地ID列表
int64 host_gid = 2; // 农场主GID
bool is_all = 3; // 是否全部收获
}
message HarvestReply {
repeated LandInfo land = 1;
// repeated Item items = 2; // corepb.Item
// repeated Item lost_items = 3;
repeated OperationLimit operation_limits = 4;
}
// --- 浇水 ---
message WaterLandRequest {
repeated int64 land_ids = 1;
int64 host_gid = 2;
}
message WaterLandReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
}
// --- 除草 ---
message WeedOutRequest {
repeated int64 land_ids = 1;
int64 host_gid = 2;
}
message WeedOutReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
}
// --- 除虫 ---
message InsecticideRequest {
repeated int64 land_ids = 1;
int64 host_gid = 2;
}
message InsecticideReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
}
// --- 播种 ---
message PlantItem {
int64 seed_id = 1; // 种子ID
repeated int64 land_ids = 2; // 要种植的土地ID列表
bool auto_slave = 3; // 是否自动副产
}
message PlantRequest {
map<int64, int64> land_and_seed = 1; // land_id -> seed_id (旧版)
repeated PlantItem items = 2; // 新版: 按种子分组种植
}
message PlantReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
}
// --- 移除植物 ---
message RemovePlantRequest {
repeated int64 land_ids = 1;
}
message RemovePlantReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
}
// --- 施肥 ---
message FertilizeRequest {
repeated int64 land_ids = 1;
int64 fertilizer_id = 2;
}
message FertilizeReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
int64 fertilizer = 3; // 剩余肥料时间
}
// --- 升级土地 ---
message UpgradeLandRequest {
int64 land_id = 1;
}
message UpgradeLandReply {
LandInfo land = 1;
}
// --- 解锁土地 ---
message UnlockLandRequest {
int64 land_id = 1;
bool do_shared = 2;
}
message UnlockLandReply {
LandInfo land = 1;
}
// --- 放虫 ---
message PutInsectsRequest {
repeated int64 land_ids = 1;
int64 host_gid = 2;
}
message PutInsectsReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
}
// --- 放草 ---
message PutWeedsRequest {
repeated int64 land_ids = 1;
int64 host_gid = 2;
}
message PutWeedsReply {
repeated LandInfo land = 1;
repeated OperationLimit operation_limits = 2;
}
// ============ 服务器推送通知 ============
// 土地状态变化通知 (被放虫/放草/偷菜等)
message LandsNotify {
repeated LandInfo lands = 1; // 变化的土地列表
int64 host_gid = 2; // 农场主GID
}

View File

@@ -0,0 +1,74 @@
syntax = "proto3";
package gamepb.shoppb;
import "corepb.proto";
// ============ 商店类型: 1=道具商店, 2=种子商店, 3=宠物商店 ============
// ============ 商店概览 ============
message ShopProfile {
int64 shop_id = 1; // 商店ID
string shop_name = 2; // 商店名称
int32 shop_type = 3; // 商店类型
}
// ============ 商品信息 ============
message GoodsInfo {
int64 id = 1; // 商品ID (用于购买)
int64 bought_num = 2; // 已购买数量
int64 price = 3; // 价格 (金币)
int64 limit_count = 4; // 限购数量 (0=不限购)
bool unlocked = 5; // 是否已解锁
int64 item_id = 6; // 物品ID (即种子ID)
int64 item_count = 7; // 每次购买获得数量
repeated Cond conds = 8; // 解锁条件
}
// ============ 解锁条件 ============
enum CondType {
COND_TYPE_UNKNOWN = 0;
MIN_LEVEL = 1; // 最低等级要求
UNLOCK_CARD = 2; // 需要解锁卡
}
message Cond {
int32 type = 1; // CondType
int64 param = 2; // 参数值 (如等级要求的等级数)
}
// ============ 请求/回复 ============
// --- 获取商店列表 ---
message ShopProfilesRequest {}
message ShopProfilesReply {
repeated ShopProfile shop_profiles = 1;
}
// --- 获取商店商品 ---
message ShopInfoRequest {
int64 shop_id = 1;
}
message ShopInfoReply {
repeated GoodsInfo goods_list = 1;
}
// --- 购买商品 ---
message BuyGoodsRequest {
int64 goods_id = 1; // 商品ID (GoodsInfo.id)
int64 num = 2; // 购买数量
int64 price = 3; // 单价
}
message BuyGoodsReply {
GoodsInfo goods = 1; // 更新后的商品信息
repeated corepb.Item get_items = 2; // 获得的物品
repeated corepb.Item cost_items = 3; // 消耗的物品
}
// --- 商品解锁推送 ---
message GoodsUnlockNotify {
repeated GoodsInfo goods_list = 1;
}

View File

@@ -0,0 +1,81 @@
syntax = "proto3";
package gamepb.taskpb;
import "corepb.proto";
// ============ 任务类型 ============
// 1 = 成长任务 (GROWTH_TASK)
// 2 = 每日任务 (DAILY_TASK)
// ============ 单个任务 ============
message Task {
int64 id = 1; // 任务ID
int64 progress = 2; // 当前进度
bool is_claimed = 3; // 是否已领取
bool is_unlocked = 4; // 是否已解锁
repeated corepb.Item rewards = 5; // 奖励列表
int64 total_progress = 6; // 总进度
int64 share_multiple = 7; // 分享倍数 (>1 表示可分享翻倍)
repeated string params = 8; // 参数
string desc = 9; // 描述
int32 task_type = 10; // 任务类型
int64 group = 11; // 分组
int64 cond_type = 12; // 条件类型
int64 is_show_text = 13; // 是否显示文本
}
// ============ 活跃度奖励 ============
message Active {
int64 id = 1;
int32 status = 2; // 0=未完成, 1=已完成, 2=未完成
repeated corepb.Item items = 3;
}
// ============ 任务信息汇总 ============
message TaskInfo {
repeated Task growth_tasks = 1; // 成长任务列表
repeated Task daily_tasks = 2; // 每日任务列表
repeated Task tasks = 3; // 其他任务
repeated Active actives = 4; // 活跃度奖励
}
// ============ 请求/回复 ============
// --- 获取任务信息 ---
message TaskInfoRequest {}
message TaskInfoReply {
TaskInfo task_info = 1;
}
// --- 领取单个任务奖励 ---
message ClaimTaskRewardRequest {
int64 id = 1; // 任务ID
bool do_shared = 2; // 是否使用分享翻倍 (true=翻倍, false=普通领取)
}
message ClaimTaskRewardReply {
repeated corepb.Item items = 1; // 获得的物品
TaskInfo task_info = 2; // 更新后的任务信息
repeated corepb.Item compensated_items = 3; // 补偿物品
}
// --- 批量领取任务奖励 ---
message BatchClaimTaskRewardRequest {
repeated int64 ids = 1; // 任务ID列表
bool do_shared = 2; // 是否使用分享翻倍
}
message BatchClaimTaskRewardReply {
repeated corepb.Item items = 1;
TaskInfo task_info = 2;
repeated corepb.Item compensated_items = 3;
}
// ============ 服务器推送 ============
// 任务状态变化通知
message TaskInfoNotify {
TaskInfo task_info = 1;
}

View File

@@ -0,0 +1,123 @@
syntax = "proto3";
package gamepb.userpb;
// ============ 登录请求 ============
message LoginRequest {
// 字段 1,2 保留未使用
int64 sharer_id = 3;
string sharer_open_id = 4;
DeviceInfo device_info = 5;
int64 share_cfg_id = 6;
string scene_id = 7;
ReportData report_data = 8;
}
// 设备信息
message DeviceInfo {
string client_version = 1; // e.g. "1.6.0.8_20251224"
string sys_software = 2; // e.g. "Windows Unknown x64"
string sys_hardware = 3;
string telecom_oper = 4;
string network = 5; // e.g. "wifi"
int64 screen_width = 6;
int64 screen_height = 7;
float density = 8;
string cpu = 9; // e.g. "microsoft"
int64 memory = 10;
string gl_render = 11;
string gl_version = 12;
string device_id = 13;
string android_oaid = 14;
string ios_caid = 15;
}
// 上报数据
message ReportData {
string callback = 1;
string cd_extend_info = 2;
string click_id = 3;
string clue_token = 4;
string minigame_channel = 5;
int32 minigame_platid = 6;
string req_id = 7;
string trackid = 8;
}
// ============ 登录回复 ============
message LoginReply {
BasicInfo basic = 1;
// ItemBag item_bag = 2; // corepb.ItemBag, 复杂结构先跳过
int64 time_now_millis = 3;
bool is_first_login = 4;
// GuideInfo guide_info = 5;
repeated QQGroupInfo qq_group_infos = 6;
// Illustrated illustrated = 7;
// repeated SystemUnlockItem unlocked_items = 8;
VersionInfo version_info = 9;
// MallMsg mall_msg = 10;
int64 qq_friend_recommend_authorized = 11;
}
// 用户基本信息
message BasicInfo {
int64 gid = 1;
string name = 2;
int64 level = 3;
int64 exp = 4;
int64 gold = 5;
string open_id = 6;
string avatar_url = 7;
string remark = 8;
string signature = 9;
int32 gender = 10;
// repeated AvatarFrame equip_avatar_frames = 11;
// map<int64, ShareInfo> share_infos = 12;
int32 authorized_status = 13;
bool disable_nudge = 14;
}
// QQ群信息
message QQGroupInfo {
string qq_group_id = 1;
string qq_group_name = 2;
}
// 版本信息
message VersionInfo {
int32 status = 1;
string version_recommend = 2;
string version_force = 3;
string res_version = 4;
}
// ============ 心跳 ============
message HeartbeatRequest {
int64 gid = 1;
string client_version = 2;
}
message HeartbeatReply {
int64 server_time = 1;
VersionInfo version_info = 2;
}
// ============ 上报分享点击 ============
// 用于已登录状态下处理分享链接(触发好友申请)
message ReportArkClickRequest {
int64 sharer_id = 1;
string sharer_open_id = 2;
string scene_id = 3;
int64 share_cfg_id = 4;
}
message ReportArkClickReply {
// 通常为空响应
}
// ============ 服务器推送通知 ============
// 基本信息变化通知 (升级/金币变化等)
message BasicNotify {
BasicInfo basic = 1;
}

View File

@@ -0,0 +1,42 @@
syntax = "proto3";
package gamepb.visitpb;
import "plantpb.proto";
import "userpb.proto";
// ============ 访问好友农场服务 ============
// service VisitService {
// rpc Enter(EnterRequest) returns (EnterReply);
// rpc Leave(LeaveRequest) returns (LeaveReply);
// }
// ============ 进入原因枚举 ============
enum EnterReason {
ENTER_REASON_UNKNOWN = 0;
ENTER_REASON_BUBBLE = 1;
ENTER_REASON_FRIEND = 2;
ENTER_REASON_INTERACT = 3;
}
// ============ 进入好友农场 ============
message EnterRequest {
int64 host_gid = 1; // 好友的GID
int32 reason = 2; // EnterReason 进入原因
}
message EnterReply {
gamepb.userpb.BasicInfo basic = 1; // 好友基本信息
repeated gamepb.plantpb.LandInfo lands = 2; // 好友的所有土地
// field 3: brief_dog_info (不需要)
// field 4: nudge_info (不需要)
}
// ============ 离开好友农场 ============
message LeaveRequest {
int64 host_gid = 1; // 好友的GID
}
message LeaveReply {
// 空消息
}

0
211/server/share.txt Normal file
View File

53
211/server/src/config.js Normal file
View File

@@ -0,0 +1,53 @@
/**
* 配置常量与枚举定义
*/
const CONFIG = {
serverUrl: 'wss://gate-obt.nqf.qq.com/prod/ws',
clientVersion: '1.6.0.14_20251224',
platform: 'qq', // 平台: qq 或 wx (可通过 --wx 切换为微信)
os: 'iOS',
heartbeatInterval: 25000, // 心跳间隔 25秒
farmCheckInterval: 1000, // 自己农场巡查完成后等待间隔 (可通过 --interval 修改, 最低1秒)
friendCheckInterval: 10000, // 好友巡查完成后等待间隔 (可通过 --friend-interval 修改, 最低1秒)
forceLowestLevelCrop: false, // 开启后固定种最低等级作物(通常是白萝卜),跳过经验效率分析
enableNormalFertilize: false,
enableOrganicFertilize: false,
allowTicketFertilizerPurchase: false,
enableFriendOps: true,
enableSteal: true,
enableAutoSell: true,
allowBuySeeds: true,
allowRemove: true,
idleStrategy: 'exp',
};
// 运行期提示文案(做了简单编码,避免明文散落)
const RUNTIME_HINT_MASK = 23;
const RUNTIME_HINT_DATA = [
12295, 22759, 26137, 12294, 26427, 39022, 30457, 24343, 28295, 20826,
36142, 65307, 20018, 31126, 20485, 21313, 12309, 35808, 20185, 20859,
24343, 20164, 24196, 20826, 36142, 33696, 21441, 12309,
];
// 生长阶段枚举
const PlantPhase = {
UNKNOWN: 0,
SEED: 1,
GERMINATION: 2,
SMALL_LEAVES: 3,
LARGE_LEAVES: 4,
BLOOMING: 5,
MATURE: 6,
DEAD: 7,
};
const PHASE_NAMES = ['未知', '种子', '发芽', '小叶', '大叶', '开花', '成熟', '枯死'];
module.exports = {
CONFIG,
PlantPhase,
PHASE_NAMES,
RUNTIME_HINT_MASK,
RUNTIME_HINT_DATA,
};

View File

@@ -0,0 +1,186 @@
const EventEmitter = require('events');
const { NetworkClient } = require('./Network');
const { FarmManager } = require('./FarmManager');
const { FriendManager } = require('./FriendManager');
const { WarehouseManager } = require('./WarehouseManager');
const { ShopManager } = require('./ShopManager');
const { log, logWarn, emitRuntimeHint } = require('../utils');
const { CONFIG } = require('../config');
class FarmBot extends EventEmitter {
constructor(config = {}) {
super();
// Merge provided config with default CONFIG
// Deep copy to avoid modifying global CONFIG if passed by reference (though usually it's shallow copy)
this.config = { ...CONFIG, ...config };
// Initialize components
this.network = new NetworkClient(this);
this.farmManager = new FarmManager(this);
this.friendManager = new FriendManager(this);
this.warehouseManager = new WarehouseManager(this);
this.shopManager = new ShopManager(this);
this.dailyClaimTimer = null;
// State
this.isRunning = false;
this.user = {
gid: 0,
name: 'Loading...',
level: 0,
gold: 0,
exp: 0,
tickets: 0,
fertilizer: 0,
freeMallClaimDate: ''
};
// Error handling
this.network.on('error', (err) => {
log('机器人', `网络错误: ${err.message}`);
this.emit('error', err);
});
this.network.on('disconnected', () => {
if (this.isRunning) {
log('机器人', '连接断开,正在尝试重新连接...');
// Reconnection logic is handled in NetworkClient, but we can monitor it here
}
});
}
async start() {
if (this.isRunning) return;
this.isRunning = true;
this.log('系统', `正在启动农场小助手... 平台: ${this.config.platform}`);
emitRuntimeHint(true);
try {
// 1. Connect
this.log('系统', '正在连接服务器...');
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('连接超时')), 10000);
this.network.connect((success) => {
clearTimeout(timeout);
if (success) resolve();
else reject(new Error('连接失败'));
});
});
// 2. Login
this.log('系统', '正在登录...');
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('登录超时')), 10000);
this.network.sendLogin((success) => {
clearTimeout(timeout);
if (success) resolve();
else reject(new Error('登录失败'));
});
});
// 3. Start Loops
this.log('系统', '开始自动化作业...');
this.farmManager.startLoop();
if (this.config.enableFriendOps !== false) {
this.friendManager.startLoop();
}
if (this.config.enableAutoSell !== false) {
this.warehouseManager.startLoop();
}
this.startDailyClaimLoop();
this.emit('started');
this.log('系统', '农场小助手启动成功!');
} catch (error) {
this.log('系统', `启动失败: ${error.message}`);
this.stop();
throw error;
}
}
stop() {
this.isRunning = false;
this.farmManager.stopLoop();
this.friendManager.stopLoop();
this.warehouseManager.stopLoop();
this.stopDailyClaimLoop();
// Stop network
if (this.network.ws) {
this.network.ws.close();
}
this.emit('stopped');
log('系统', '农场小助手已停止');
}
log(tag, msg) {
log(tag, msg);
this.emit('log', {
tag,
msg,
time: Date.now()
});
}
logWarn(tag, msg) {
logWarn(tag, msg);
this.emit('log', {
tag,
msg,
type: 'warn',
time: Date.now()
});
}
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 runDailyClaims() {
if (!this.isRunning) return;
await this.network.tryClaimShareReward(1);
const todayKey = this.getTodayKey();
if (this.user.freeMallClaimDate === todayKey) return;
try {
await this.shopManager.purchaseMallItem(1001, 1);
this.user.freeMallClaimDate = todayKey;
this.emit('userUpdate', this.user);
this.log('商城', '已领取每日免费礼包');
} catch (e) {
this.logWarn('商城', `领取失败: ${e.message}`);
}
}
startDailyClaimLoop() {
if (this.dailyClaimTimer) return;
const scheduleNext = () => {
if (!this.isRunning) return;
const now = new Date();
const next = new Date(now);
next.setHours(24, 0, 0, 0);
const delay = Math.max(1000, next.getTime() - now.getTime());
this.dailyClaimTimer = setTimeout(async () => {
this.dailyClaimTimer = null;
await this.runDailyClaims();
scheduleNext();
}, delay);
};
scheduleNext();
}
stopDailyClaimLoop() {
if (this.dailyClaimTimer) {
clearTimeout(this.dailyClaimTimer);
this.dailyClaimTimer = null;
}
}
}
module.exports = { FarmBot };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,497 @@
const { PlantPhase } = require('../config');
const { types } = require('../proto');
const { toLong, toNum, sleep, getServerTimeSec, toTimeSec } = require('../utils');
const { getPlantName } = require('../gameConfig');
const OP_NAMES = {
10001: '收获', 10002: '铲除', 10003: '放草', 10004: '放虫',
10005: '除草', 10006: '除虫', 10007: '浇水', 10008: '偷菜',
};
class FriendManager {
constructor(bot) {
this.bot = bot;
// Internal state
this.isCheckingFriends = false;
this.isFirstFriendCheck = true;
this.friendCheckTimer = null;
this.friendLoopRunning = false;
this.lastResetDate = '';
this.operationLimits = new Map();
// Listen for events
this.bot.on('operationLimits', this.updateOperationLimits.bind(this));
this.bot.on('friendApplicationReceived', this.onFriendApplicationReceived.bind(this));
}
// ============ Helper Methods ============
checkDailyReset() {
const today = new Date().toISOString().slice(0, 10);
if (this.lastResetDate !== today) {
if (this.lastResetDate !== '') {
this.bot.log('系统', '跨日重置,已清空操作限制缓存');
}
this.operationLimits.clear();
this.lastResetDate = today;
}
}
updateOperationLimits(limits) {
if (!limits || limits.length === 0) return;
this.checkDailyReset();
for (const limit of limits) {
const id = toNum(limit.id);
if (id > 0) {
this.operationLimits.set(id, {
dayTimes: toNum(limit.day_times),
dayTimesLimit: toNum(limit.day_times_lt),
dayExpTimes: toNum(limit.day_exp_times),
dayExpTimesLimit: toNum(limit.day_ex_times_lt),
});
}
}
}
canGetExp(opId) {
const limit = this.operationLimits.get(opId);
if (!limit) return false;
if (limit.dayExpTimesLimit <= 0) return true;
return limit.dayExpTimes < limit.dayExpTimesLimit;
}
canOperate(opId) {
const limit = this.operationLimits.get(opId);
if (!limit) return true;
if (limit.dayTimesLimit <= 0) return true;
return limit.dayTimes < limit.dayTimesLimit;
}
getRemainingTimes(opId) {
const limit = this.operationLimits.get(opId);
if (!limit || limit.dayTimesLimit <= 0) return 999;
return Math.max(0, limit.dayTimesLimit - limit.dayTimes);
}
// ============ Friend API ============
async getAllFriends() {
const body = types.GetAllFriendsRequest.encode(types.GetAllFriendsRequest.create({})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'GetAll', body);
return types.GetAllFriendsReply.decode(replyBody);
}
async getApplications() {
const body = types.GetApplicationsRequest.encode(types.GetApplicationsRequest.create({})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'GetApplications', body);
return types.GetApplicationsReply.decode(replyBody);
}
async acceptFriends(gids) {
const body = types.AcceptFriendsRequest.encode(types.AcceptFriendsRequest.create({
friend_gids: gids.map(g => toLong(g)),
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.friendpb.FriendService', 'AcceptFriends', body);
return types.AcceptFriendsReply.decode(replyBody);
}
async enterFriendFarm(friendGid) {
const body = types.VisitEnterRequest.encode(types.VisitEnterRequest.create({
host_gid: toLong(friendGid),
reason: 2,
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.visitpb.VisitService', 'Enter', body);
return types.VisitEnterReply.decode(replyBody);
}
async leaveFriendFarm(friendGid) {
const body = types.VisitLeaveRequest.encode(types.VisitLeaveRequest.create({
host_gid: toLong(friendGid),
})).finish();
try {
await this.bot.network.sendMsgAsync('gamepb.visitpb.VisitService', 'Leave', body);
} catch (e) { }
}
async helpWater(friendGid, landIds) {
const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body);
const reply = types.WaterLandReply.decode(replyBody);
this.updateOperationLimits(reply.operation_limits);
return reply;
}
async helpWeed(friendGid, landIds) {
const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body);
const reply = types.WeedOutReply.decode(replyBody);
this.updateOperationLimits(reply.operation_limits);
return reply;
}
async helpInsecticide(friendGid, landIds) {
const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body);
const reply = types.InsecticideReply.decode(replyBody);
this.updateOperationLimits(reply.operation_limits);
return reply;
}
async stealHarvest(friendGid, landIds) {
const body = types.HarvestRequest.encode(types.HarvestRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
is_all: true,
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body);
const reply = types.HarvestReply.decode(replyBody);
this.updateOperationLimits(reply.operation_limits);
return reply;
}
async putInsects(friendGid, landIds) {
const body = types.PutInsectsRequest.encode(types.PutInsectsRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'PutInsects', body);
const reply = types.PutInsectsReply.decode(replyBody);
this.updateOperationLimits(reply.operation_limits);
return reply;
}
async putWeeds(friendGid, landIds) {
const body = types.PutWeedsRequest.encode(types.PutWeedsRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.plantpb.PlantService', 'PutWeeds', body);
const reply = types.PutWeedsReply.decode(replyBody);
this.updateOperationLimits(reply.operation_limits);
return reply;
}
// ============ Logic ============
getCurrentPhase(phases) {
if (!phases || phases.length === 0) return null;
const nowSec = getServerTimeSec();
for (let i = phases.length - 1; i >= 0; i--) {
const beginTime = toTimeSec(phases[i].begin_time);
if (beginTime > 0 && beginTime <= nowSec) {
return phases[i];
}
}
return phases[0];
}
analyzeFriendLands(lands, myGid) {
const result = {
stealable: [], stealableInfo: [],
needWater: [], needWeed: [], needBug: [],
canPutWeed: [], canPutBug: [],
};
const nowSec = getServerTimeSec();
for (const land of lands) {
const id = toNum(land.id);
const plant = land.plant;
if (!plant || !plant.phases || plant.phases.length === 0) continue;
const currentPhase = this.getCurrentPhase(plant.phases);
if (!currentPhase) continue;
const phaseVal = currentPhase.phase;
if (phaseVal === PlantPhase.MATURE) {
if (plant.stealable) {
result.stealable.push(id);
const plantId = toNum(plant.id);
const plantName = getPlantName(plantId) || plant.name || '未知';
result.stealableInfo.push({ landId: id, plantId, name: plantName });
}
continue;
}
if (phaseVal === PlantPhase.DEAD) continue;
const dryNum = toNum(plant.dry_num);
const dryTime = toTimeSec(currentPhase.dry_time);
if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) result.needWater.push(id);
const weedsTime = toTimeSec(currentPhase.weeds_time);
const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec);
if (hasWeeds) result.needWeed.push(id);
const insectTime = toTimeSec(currentPhase.insect_time);
const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec);
if (hasBugs) result.needBug.push(id);
const weedOwners = plant.weed_owners || [];
const insectOwners = plant.insect_owners || [];
const iAlreadyPutWeed = weedOwners.some(gid => toNum(gid) === myGid);
const iAlreadyPutBug = insectOwners.some(gid => toNum(gid) === myGid);
if (weedOwners.length < 2 && !iAlreadyPutWeed) result.canPutWeed.push(id);
if (insectOwners.length < 2 && !iAlreadyPutBug) result.canPutBug.push(id);
}
return result;
}
async visitFriend(friend, totalActions, myGid) {
const { gid, name } = friend;
let enterReply;
try {
enterReply = await this.enterFriendFarm(gid);
} catch (e) {
this.bot.logWarn('好友', `进入 ${name} 农场失败: ${e.message}`);
return;
}
const lands = enterReply.lands || [];
if (lands.length === 0) {
await this.leaveFriendFarm(gid);
return;
}
const status = this.analyzeFriendLands(lands, myGid);
const actions = [];
const HELP_ONLY_WITH_EXP = true;
// Note: Logic copied from original friend.js
if (status.needWeed.length > 0) {
const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10005);
if (shouldHelp) {
let ok = 0;
for (const landId of status.needWeed) {
try { await this.helpWeed(gid, [landId]); ok++; } catch (e) { }
await sleep(100);
}
if (ok > 0) { actions.push(`除草 ${ok}`); totalActions.weed += ok; }
}
}
if (status.needBug.length > 0) {
const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10006);
if (shouldHelp) {
let ok = 0;
for (const landId of status.needBug) {
try { await this.helpInsecticide(gid, [landId]); ok++; } catch (e) { }
await sleep(100);
}
if (ok > 0) { actions.push(`除虫 ${ok}`); totalActions.bug += ok; }
}
}
if (status.needWater.length > 0) {
const shouldHelp = !HELP_ONLY_WITH_EXP || this.canGetExp(10007);
if (shouldHelp) {
let ok = 0;
for (const landId of status.needWater) {
try { await this.helpWater(gid, [landId]); ok++; } catch (e) { }
await sleep(100);
}
if (ok > 0) { actions.push(`浇水 ${ok}`); totalActions.water += ok; }
}
}
const canSteal = !(this.bot.config && this.bot.config.enableSteal === false);
if (canSteal && status.stealable.length > 0) {
let ok = 0;
const stolenPlants = [];
for (let i = 0; i < status.stealable.length; i++) {
const landId = status.stealable[i];
try {
await this.stealHarvest(gid, [landId]);
ok++;
if (status.stealableInfo[i]) {
stolenPlants.push(status.stealableInfo[i].name);
}
} catch (e) { }
await sleep(100);
}
if (ok > 0) {
const plantNames = [...new Set(stolenPlants)].join('、');
actions.push(`偷取 ${ok}${plantNames ? ' (' + plantNames + ')' : ''}`);
totalActions.steal += ok;
this.bot.emit('stealRecord', { name, count: ok });
}
}
const ENABLE_PUT_BAD_THINGS = false; // Copied from original
if (ENABLE_PUT_BAD_THINGS && status.canPutBug.length > 0 && this.canOperate(10004)) {
let ok = 0;
const remaining = this.getRemainingTimes(10004);
const toProcess = status.canPutBug.slice(0, remaining);
for (const landId of toProcess) {
if (!this.canOperate(10004)) break;
try { await this.putInsects(gid, [landId]); ok++; } catch (e) { }
await sleep(100);
}
if (ok > 0) { actions.push(`放虫 ${ok}`); totalActions.putBug += ok; }
}
if (ENABLE_PUT_BAD_THINGS && status.canPutWeed.length > 0 && this.canOperate(10003)) {
let ok = 0;
const remaining = this.getRemainingTimes(10003);
const toProcess = status.canPutWeed.slice(0, remaining);
for (const landId of toProcess) {
if (!this.canOperate(10003)) break;
try { await this.putWeeds(gid, [landId]); ok++; } catch (e) { }
await sleep(100);
}
if (ok > 0) { actions.push(`放草 ${ok}`); totalActions.putWeed += ok; }
}
if (actions.length > 0) {
this.bot.log('好友', `[${name}] 执行: ${actions.join('')}`);
}
await this.leaveFriendFarm(gid);
}
async check() {
if (this.isCheckingFriends || !this.bot.user.gid) return;
if (this.bot.config && this.bot.config.enableFriendOps === false) return;
this.isCheckingFriends = true;
this.checkDailyReset();
try {
const friendsReply = await this.getAllFriends();
const friends = friendsReply.game_friends || [];
if (friends.length === 0) { this.bot.log('好友', '没有好友'); return; }
const canPutBugOrWeed = this.canOperate(10004) || this.canOperate(10003);
const ENABLE_PUT_BAD_THINGS = false;
const priorityFriends = [];
const otherFriends = [];
const visitedGids = new Set();
for (const f of friends) {
const gid = toNum(f.gid);
if (gid === this.bot.user.gid) continue;
if (visitedGids.has(gid)) continue;
const name = f.remark || f.name || `GID:${gid}`;
const p = f.plant;
const stealNum = p ? toNum(p.steal_plant_num) : 0;
const dryNum = p ? toNum(p.dry_num) : 0;
const weedNum = p ? toNum(p.weed_num) : 0;
const insectNum = p ? toNum(p.insect_num) : 0;
if (stealNum > 0 || dryNum > 0 || weedNum > 0 || insectNum > 0) {
priorityFriends.push({ gid, name });
visitedGids.add(gid);
} else if (ENABLE_PUT_BAD_THINGS && canPutBugOrWeed) {
otherFriends.push({ gid, name });
visitedGids.add(gid);
}
}
const friendsToVisit = [...priorityFriends, ...otherFriends];
if (friendsToVisit.length === 0) return;
let totalActions = { steal: 0, water: 0, weed: 0, bug: 0, putBug: 0, putWeed: 0 };
for (const friend of friendsToVisit) {
try {
await this.visitFriend(friend, totalActions, this.bot.user.gid);
} catch (e) { }
await sleep(500);
}
const summary = [];
if (totalActions.steal > 0) summary.push(`偷取 ${totalActions.steal}`);
if (totalActions.weed > 0) summary.push(`除草 ${totalActions.weed}`);
if (totalActions.bug > 0) summary.push(`除虫 ${totalActions.bug}`);
if (totalActions.water > 0) summary.push(`浇水 ${totalActions.water}`);
if (totalActions.putBug > 0) summary.push(`放虫 ${totalActions.putBug}`);
if (totalActions.putWeed > 0) summary.push(`放草 ${totalActions.putWeed}`);
if (summary.length > 0) {
this.bot.log('好友', `已巡查 ${friendsToVisit.length} 位好友。总计: ${summary.join('')}`);
}
this.isFirstFriendCheck = false;
} catch (err) {
this.bot.logWarn('好友', `巡查失败: ${err.message}`);
} finally {
this.isCheckingFriends = false;
}
}
async loop() {
while (this.friendLoopRunning) {
await this.check();
if (!this.friendLoopRunning) break;
await sleep(this.bot.config.friendCheckInterval);
}
}
startLoop() {
if (this.friendLoopRunning) return;
if (this.bot.config && this.bot.config.enableFriendOps === false) return;
this.friendLoopRunning = true;
this.friendCheckTimer = setTimeout(() => this.loop(), 5000);
setTimeout(() => this.checkAndAcceptApplications(), 3000);
}
stopLoop() {
this.friendLoopRunning = false;
if (this.friendCheckTimer) { clearTimeout(this.friendCheckTimer); this.friendCheckTimer = null; }
}
// ============ Applications ============
onFriendApplicationReceived(applications) {
if (this.bot.config && this.bot.config.enableFriendOps === false) return;
const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', ');
this.bot.log('申请', `收到 ${applications.length} 个好友申请: ${names}`);
const gids = applications.map(a => toNum(a.gid));
this.acceptFriendsWithRetry(gids);
}
async checkAndAcceptApplications() {
if (this.bot.config && this.bot.config.enableFriendOps === false) return;
try {
const reply = await this.getApplications();
const applications = reply.applications || [];
if (applications.length === 0) return;
const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', ');
this.bot.log('申请', `发现 ${applications.length} 个待处理申请: ${names}`);
const gids = applications.map(a => toNum(a.gid));
await this.acceptFriendsWithRetry(gids);
} catch (e) { }
}
async acceptFriendsWithRetry(gids) {
if (gids.length === 0) return;
try {
const reply = await this.acceptFriends(gids);
const friends = reply.friends || [];
if (friends.length > 0) {
const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', ');
this.bot.log('申请', `已同意 ${friends.length} 人: ${names}`);
}
} catch (e) {
this.bot.logWarn('申请', `同意失败: ${e.message}`);
}
}
}
module.exports = { FriendManager };

View File

@@ -0,0 +1,541 @@
const WebSocket = require('ws');
const EventEmitter = require('events');
const protobuf = require('protobufjs');
const { types } = require('../proto');
const { toLong, toNum, log, logWarn, syncServerTime } = require('../utils');
function calcNormalFertilizerHours(containerCount) {
const containerNum = Number(containerCount) || 0;
return containerNum >= 3600 ? containerNum / 3600 : containerNum;
}
function calcOrganicFertilizerHours(containerCount) {
const containerNum = Number(containerCount) || 0;
return containerNum >= 3600 ? containerNum / 3600 : containerNum;
}
class NetworkClient extends EventEmitter {
constructor(bot) {
super();
this.bot = bot;
this.ws = null;
this.clientSeq = 1;
this.serverSeq = 0;
this.heartbeatTimer = null;
this.pendingCallbacks = new Map();
this.connected = false;
this.shareClaiming = false;
this.lastShareClaimAt = 0;
// Heartbeat state
this.lastHeartbeatResponse = 0;
this.heartbeatMissCount = 0;
}
connect(callback) {
const code = encodeURIComponent(this.bot.config.code || '');
const url = `${this.bot.config.serverUrl}?platform=${this.bot.config.platform}&os=${this.bot.config.os}&ver=${this.bot.config.clientVersion}&code=${code}&openID=`;
this.bot.log('系统', `连接服务器... ${this.bot.config.platform}`);
this.ws = new WebSocket(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13)',
'Origin': 'https://gate-obt.nqf.qq.com',
},
});
this.ws.binaryType = 'arraybuffer';
this.ws.on('open', () => {
this.bot.log('系统', 'WebSocket 连接成功');
this.connected = true;
if (callback) callback(true);
});
this.ws.on('message', (data) => {
this.handleMessage(data);
});
this.ws.on('close', (code, reason) => {
this.bot.log('系统', `WebSocket 连接断开 (code=${code})`);
this.connected = false;
this.stopHeartbeat();
this.cleanup();
this.emit('disconnected');
});
this.ws.on('error', (err) => {
this.bot.logWarn('系统', `WebSocket 错误: ${err.message}`);
this.emit('error', err);
});
}
cleanup() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
this.pendingCallbacks.clear();
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.cleanup();
}
startHeartbeat() {
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
this.lastHeartbeatResponse = Date.now();
this.heartbeatMissCount = 0;
this.heartbeatTimer = setInterval(() => {
if (!this.bot.user.gid) return;
const timeSinceLastResponse = Date.now() - this.lastHeartbeatResponse;
if (timeSinceLastResponse > 60000) {
this.heartbeatMissCount++;
this.bot.logWarn('心跳', `连接可能已断开 (${Math.round(timeSinceLastResponse/1000)}s 无响应)`);
if (this.heartbeatMissCount >= 2) {
this.bot.log('心跳', '尝试重连...');
// Cleanup pending callbacks to avoid leaks
this.pendingCallbacks.forEach((cb, seq) => {
try { cb(new Error('连接超时,已清理')); } catch (e) {}
});
this.pendingCallbacks.clear();
// Optional: trigger reconnect logic here or let the user handle it
}
}
const body = types.HeartbeatRequest.encode(types.HeartbeatRequest.create({
gid: toLong(this.bot.user.gid),
client_version: this.bot.config.clientVersion,
})).finish();
this.sendMsg('gamepb.userpb.UserService', 'Heartbeat', body, (err, replyBody) => {
if (err || !replyBody) return;
this.lastHeartbeatResponse = Date.now();
this.heartbeatMissCount = 0;
try {
const reply = types.HeartbeatReply.decode(replyBody);
if (reply.server_time) {
// We still use the global syncServerTime for now as it's just a time offset
syncServerTime(toNum(reply.server_time));
}
} catch (e) { }
});
}, this.bot.config.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
sendLogin(callback) {
const body = types.LoginRequest.encode(types.LoginRequest.create({
sharer_id: toLong(0),
sharer_open_id: '',
device_info: {
client_version: this.bot.config.clientVersion,
sys_software: 'iOS 26.2.1',
network: 'wifi',
memory: '7672',
device_id: 'iPhone X<iPhone18,3>',
},
share_cfg_id: toLong(0),
scene_id: '1256',
report_data: {
callback: '', cd_extend_info: '', click_id: '', clue_token: '',
minigame_channel: 'other', minigame_platid: 2, req_id: '', trackid: '',
},
})).finish();
this.sendMsg('gamepb.userpb.UserService', 'Login', body, (err, bodyBytes, meta) => {
if (err) {
this.bot.log('登录', `失败: ${err.message}`);
if (callback) callback(false);
return;
}
try {
const reply = types.LoginReply.decode(bodyBytes);
if (reply.basic) {
const basic = reply.basic;
this.bot.user.gid = toNum(basic.gid);
this.bot.user.name = basic.name || '未知';
this.bot.user.level = toNum(basic.level);
this.bot.user.gold = toNum(basic.gold);
this.bot.user.exp = toNum(basic.exp);
this.bot.user.avatarUrl = basic.avatar_url;
this.bot.log('系统', `登录成功: ${this.bot.user.name} (Lv${this.bot.user.level})`);
if (reply.time_now_millis) {
syncServerTime(toNum(reply.time_now_millis));
}
}
this.startHeartbeat();
this.bot.emit('loginSuccess', this.bot.user);
this.tryClaimShareReward();
if (this.bot.warehouseManager) {
this.bot.warehouseManager.getBag().then((bagReply) => {
const items = this.bot.warehouseManager.getBagItems(bagReply);
let tickets = 0;
let fertilizerContainer = 0;
let organicFertilizerContainer = 0;
const fertilizerItems = {
80001: 0,
80002: 0,
80003: 0,
80004: 0,
};
const organicFertilizerItems = {
80011: 0,
80012: 0,
80013: 0,
80014: 0,
};
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
if (id === 1002) tickets = count;
if (id === 1011) fertilizerContainer = count;
if (id === 1012) organicFertilizerContainer = count;
if (id === 80001) fertilizerItems[80001] = count;
if (id === 80002) fertilizerItems[80002] = count;
if (id === 80003) fertilizerItems[80003] = count;
if (id === 80004) fertilizerItems[80004] = count;
if (id === 80011) organicFertilizerItems[80011] = count;
if (id === 80012) organicFertilizerItems[80012] = count;
if (id === 80013) organicFertilizerItems[80013] = count;
if (id === 80014) organicFertilizerItems[80014] = count;
}
this.bot.user.tickets = tickets;
this.bot.user.fertilizerContainer = fertilizerContainer;
this.bot.user.fertilizerItems = fertilizerItems;
this.bot.user.fertilizerHours = calcNormalFertilizerHours(fertilizerContainer);
this.bot.user.organicFertilizerContainer = organicFertilizerContainer;
this.bot.user.organicFertilizerItems = organicFertilizerItems;
this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(organicFertilizerContainer);
this.bot.emit('userUpdate', this.bot.user);
}).catch(() => { });
}
if (callback) callback(true);
} catch (e) {
this.bot.log('登录', `解码失败: ${e.message}`);
if (callback) callback(false);
}
});
}
async tryClaimShareReward(shareId = 1) {
if (this.shareClaiming) return;
const now = Date.now();
if (now - this.lastShareClaimAt < 60000) return;
this.shareClaiming = true;
this.lastShareClaimAt = now;
try {
const canShare = await this.checkCanShareReward();
if (!canShare) return;
await this.reportShare(shareId);
await this.claimShareReward(shareId);
this.bot.log('分享', '已领取分享奖励');
} catch (e) {
const message = this.formatShareClaimError(e);
this.bot.logWarn('分享', message);
} finally {
this.shareClaiming = false;
}
}
formatShareClaimError(error) {
const raw = error?.message ? String(error.message) : '';
if (!raw) return '分享奖励领取失败';
if (raw.includes('code=1009001') || raw.includes('已经领取')) {
return '分享奖励已领取,无需重复领取';
}
return `领取失败: ${raw}`;
}
decodeShareFlag(replyBody) {
if (!replyBody || replyBody.length === 0) return 0;
const reader = protobuf.Reader.create(replyBody);
let flag = 0;
while (reader.pos < reader.len) {
const tag = reader.uint32();
const field = tag >>> 3;
if (field === 1) {
flag = Number(reader.int64());
} else {
reader.skipType(tag & 7);
}
}
return flag;
}
async checkCanShareReward() {
const { body: replyBody } = await this.sendMsgAsync('gamepb.sharepb.ShareService', 'CheckCanShare', Buffer.alloc(0));
const flag = this.decodeShareFlag(replyBody);
return flag === 1;
}
async reportShare(shareId) {
const writer = protobuf.Writer.create();
writer.uint32(8).int64(toLong(shareId));
const body = writer.finish();
await this.sendMsgAsync('gamepb.sharepb.ShareService', 'ReportShare', body);
}
async claimShareReward(shareId) {
const writer = protobuf.Writer.create();
writer.uint32(8).int64(toLong(shareId));
const body = writer.finish();
await this.sendMsgAsync('gamepb.sharepb.ShareService', 'ClaimShareReward', body);
}
encodeMsg(serviceName, methodName, bodyBytes) {
const msg = types.GateMessage.create({
meta: {
service_name: serviceName,
method_name: methodName,
message_type: 1,
client_seq: toLong(this.clientSeq),
server_seq: toLong(this.serverSeq),
},
body: bodyBytes || Buffer.alloc(0),
});
const encoded = types.GateMessage.encode(msg).finish();
this.clientSeq++;
return encoded;
}
sendMsg(serviceName, methodName, bodyBytes, callback) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return false;
}
const seq = this.clientSeq;
const encoded = this.encodeMsg(serviceName, methodName, bodyBytes);
if (callback) this.pendingCallbacks.set(seq, callback);
this.ws.send(encoded);
return true;
}
sendMsgAsync(serviceName, methodName, bodyBytes, timeout = 10000) {
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
reject(new Error(`连接未打开: ${methodName}`));
return;
}
const seq = this.clientSeq;
const timer = setTimeout(() => {
this.pendingCallbacks.delete(seq);
reject(new Error(`请求超时: ${methodName}`));
}, timeout);
const sent = this.sendMsg(serviceName, methodName, bodyBytes, (err, body, meta) => {
clearTimeout(timer);
if (err) reject(err);
else resolve({ body, meta });
});
if (!sent) {
clearTimeout(timer);
reject(new Error(`发送失败: ${methodName}`));
}
});
}
handleMessage(data) {
try {
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
const msg = types.GateMessage.decode(buf);
const meta = msg.meta;
if (!meta) return;
if (meta.server_seq) {
const seq = toNum(meta.server_seq);
if (seq > this.serverSeq) this.serverSeq = seq;
}
const msgType = meta.message_type;
// Notify
if (msgType === 3) {
this.handleNotify(msg);
return;
}
// Response
if (msgType === 2) {
const errorCode = toNum(meta.error_code);
const clientSeqVal = toNum(meta.client_seq);
const cb = this.pendingCallbacks.get(clientSeqVal);
if (cb) {
this.pendingCallbacks.delete(clientSeqVal);
if (errorCode !== 0) {
cb(new Error(`${meta.service_name}.${meta.method_name} 错误: code=${errorCode} ${meta.error_message || ''}`));
} else {
cb(null, msg.body, meta);
}
return;
}
}
} catch (err) {
this.bot.logWarn('解码', err.message);
}
}
handleNotify(msg) {
if (!msg.body || msg.body.length === 0) return;
try {
const event = types.EventMessage.decode(msg.body);
const type = event.message_type || '';
const eventBody = event.body;
if (type.includes('Kickout')) {
this.bot.logWarn('推送', `被踢下线! ${type}`);
this.bot.emit('kickout', type);
return;
}
if (type.includes('LandsNotify')) {
const notify = types.LandsNotify.decode(eventBody);
const hostGid = toNum(notify.host_gid);
const lands = notify.lands || [];
if (lands.length > 0) {
if (hostGid === this.bot.user.gid || hostGid === 0) {
this.bot.emit('landsChanged', lands);
}
}
return;
}
if (type.includes('BasicNotify')) {
try {
const notify = types.BasicNotify.decode(eventBody);
if (notify.basic) {
const oldLevel = this.bot.user.level;
this.bot.user.level = toNum(notify.basic.level) || this.bot.user.level;
this.bot.user.gold = toNum(notify.basic.gold) || this.bot.user.gold;
const exp = toNum(notify.basic.exp);
if (exp > 0) this.bot.user.exp = exp;
if (this.bot.user.level !== oldLevel) {
this.bot.log('系统', `升级! Lv${oldLevel} → Lv${this.bot.user.level}`);
}
this.bot.emit('userUpdate', this.bot.user);
}
} catch (e) { }
return;
}
if (type.includes('FriendApplicationReceivedNotify')) {
try {
const notify = types.FriendApplicationReceivedNotify.decode(eventBody);
const applications = notify.applications || [];
if (applications.length > 0) {
this.bot.emit('friendApplicationReceived', applications);
}
} catch (e) { }
return;
}
if (type.includes('FriendAddedNotify')) {
try {
const notify = types.FriendAddedNotify.decode(eventBody);
const friends = notify.friends || [];
if (friends.length > 0) {
const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', ');
this.bot.log('好友', `新好友: ${names}`);
}
} catch (e) { }
return;
}
if (type.includes('ItemNotify')) {
const notify = types.ItemNotify.decode(eventBody);
const items = notify.items || [];
for (const itemChg of items) {
if (!itemChg.item) continue;
const id = toNum(itemChg.item.id);
const count = toNum(itemChg.item.count);
const delta = toNum(itemChg.delta);
if (id === 1 || id === 1001) {
this.bot.user.gold = count;
if (delta !== 0) {
this.bot.log('物品', `金币 ${delta > 0 ? '+' : ''}${delta} (当前: ${count})`);
}
this.bot.emit('userUpdate', this.bot.user);
continue;
}
if (id === 1002) {
this.bot.user.tickets = count;
this.bot.emit('userUpdate', this.bot.user);
continue;
}
if (id === 1011) {
this.bot.user.fertilizerContainer = count;
this.bot.user.fertilizerHours = calcNormalFertilizerHours(this.bot.user.fertilizerContainer);
this.bot.emit('userUpdate', this.bot.user);
continue;
}
if (id === 1012) {
this.bot.user.organicFertilizerContainer = count;
this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(this.bot.user.organicFertilizerContainer);
this.bot.emit('userUpdate', this.bot.user);
continue;
}
if (id === 80001 || id === 80002 || id === 80003 || id === 80004) {
if (!this.bot.user.fertilizerItems) {
this.bot.user.fertilizerItems = { 80001: 0, 80002: 0, 80003: 0, 80004: 0 };
}
this.bot.user.fertilizerItems[id] = count;
this.bot.user.fertilizerHours = calcNormalFertilizerHours(this.bot.user.fertilizerContainer);
this.bot.emit('userUpdate', this.bot.user);
}
if (id === 80011 || id === 80012 || id === 80013 || id === 80014) {
if (!this.bot.user.organicFertilizerItems) {
this.bot.user.organicFertilizerItems = { 80011: 0, 80012: 0, 80013: 0, 80014: 0 };
}
this.bot.user.organicFertilizerItems[id] = count;
this.bot.user.organicFertilizerHours = calcOrganicFertilizerHours(this.bot.user.organicFertilizerContainer);
this.bot.emit('userUpdate', this.bot.user);
}
}
return;
}
if (type.includes('GoodsUnlockNotify')) {
try {
const notify = types.GoodsUnlockNotify.decode(eventBody);
const goods = notify.goods_list || [];
if (goods.length > 0) {
this.bot.log('商店', `解锁 ${goods.length} 个新商品!`);
}
} catch (e) { }
return;
}
if (type.includes('TaskInfoNotify')) {
try {
const notify = types.TaskInfoNotify.decode(eventBody);
if (notify.task_info) {
this.bot.emit('taskInfoNotify', notify.task_info);
}
} catch (e) { }
return;
}
} catch (e) { }
}
}
module.exports = { NetworkClient };

View File

@@ -0,0 +1,80 @@
const protobuf = require('protobufjs');
const { types } = require('../proto');
const { toLong, toNum, log } = require('../utils');
const { getPlantNameBySeedId, getItemName } = require('../gameConfig');
class ShopManager {
constructor(bot) {
this.bot = bot;
}
async getShopProfiles() {
const body = types.ShopProfilesRequest.encode(types.ShopProfilesRequest.create({})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopProfiles', body);
return types.ShopProfilesReply.decode(replyBody);
}
async getShopInfo(shopId) {
const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({
shop_id: toLong(shopId)
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body);
return types.ShopInfoReply.decode(replyBody);
}
async buyGoods(goodsId, count, price) {
const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({
goods_id: toLong(goodsId),
num: toLong(count),
price: toLong(price)
})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body);
return types.BuyGoodsReply.decode(replyBody);
}
buildMallPurchaseBody(itemId, count) {
const writer = protobuf.Writer.create();
writer.uint32(8).int64(toLong(itemId));
writer.uint32(16).int64(toLong(count));
return writer.finish();
}
async purchaseMallItem(itemId, count) {
const body = this.buildMallPurchaseBody(itemId, count);
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.mallpb.MallService', 'Purchase', body);
return replyBody;
}
async getSeedShopList() {
// 1. 获取商店列表,找到种子商店 (type=2)
const profilesReply = await this.getShopProfiles();
const profiles = profilesReply.shop_profiles || [];
const seedShop = profiles.find(p => p.shop_type === 2); // 2 usually means Seed Shop
if (!seedShop) {
throw new Error('未找到种子商店');
}
// 2. 获取商品列表
const infoReply = await this.getShopInfo(seedShop.shop_id);
const goodsList = infoReply.goods_list || [];
// 3. 格式化数据
return goodsList.map(g => {
const itemId = toNum(g.item_id);
const name = getPlantNameBySeedId(itemId) || getItemName(itemId) || `商品${g.id}`;
return {
goodsId: toNum(g.id),
itemId: itemId,
name: name,
price: toNum(g.price),
limitCount: toNum(g.limit_count),
boughtNum: toNum(g.bought_num),
unlocked: g.unlocked,
itemCount: toNum(g.item_count)
};
});
}
}
module.exports = { ShopManager };

View File

@@ -0,0 +1,322 @@
const protobuf = require('protobufjs');
const { types } = require('../proto');
const { toLong, toNum, log, logWarn, emitRuntimeHint } = require('../utils');
const { getFruitName, getPlantNameBySeedId, getItemNameById } = require('../gameConfig');
const seedShopData = require('../../tools/seed-shop-merged-export.json');
// 游戏内金币和点券的物品 ID (GlobalData.GodItemId / DiamondItemId)
const GOLD_ITEM_ID = 1001;
const FRUIT_ID_SET = new Set(
((seedShopData && seedShopData.rows) || [])
.map(row => Number(row.fruitId))
.filter(Number.isFinite)
);
class WarehouseManager {
constructor(bot) {
this.bot = bot;
this.sellTimer = null;
this.sellInterval = 60000;
}
isFruitIdBySeedData(id) {
return FRUIT_ID_SET.has(toNum(id));
}
/**
* 从 SellReply 中提取获得的金币数量
* 新版 SellReply 返回 get_items (repeated Item),其中 id=1001 为金币
*/
extractGold(sellReply) {
if (sellReply.get_items && sellReply.get_items.length > 0) {
for (const item of sellReply.get_items) {
const id = toNum(item.id);
if (id === GOLD_ITEM_ID) {
return toNum(item.count);
}
}
return 0;
}
if (sellReply.gold !== undefined && sellReply.gold !== null) {
return toNum(sellReply.gold);
}
return 0;
}
async getBag() {
const body = types.BagRequest.encode(types.BagRequest.create({})).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Bag', body);
return types.BagReply.decode(replyBody);
}
/**
* 将 item 转为 Sell 请求所需格式id/count/uid 保留 Long 或转成 Long与游戏一致
*/
toSellItem(item) {
const id = item.id != null ? toLong(item.id) : undefined;
const count = item.count != null ? toLong(item.count) : undefined;
const uid = item.uid != null ? toLong(item.uid) : undefined;
return { id, count, uid };
}
async sellItems(items) {
const payload = items.map(this.toSellItem);
const body = types.SellRequest.encode(types.SellRequest.create({ items: payload })).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Sell', body);
return types.SellReply.decode(replyBody);
}
/**
* 从 BagReply 取出物品列表(兼容 item_bag 与旧版 items
*/
getBagItems(bagReply) {
if (bagReply.item_bag && bagReply.item_bag.items && bagReply.item_bag.items.length)
return bagReply.item_bag.items;
return bagReply.items || [];
}
/**
* 获取格式化的背包数据,用于前端显示
*/
async getFormattedBag() {
const bagReply = await this.getBag();
const items = this.getBagItems(bagReply);
const seeds = [];
const produce = [];
const others = [];
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
const uid = item.uid ? toNum(item.uid) : 0;
// Basic item object
const itemObj = {
id,
uid,
count,
name: `物品 ${id}`,
type: 'other'
};
if (this.isFruitIdBySeedData(id) || id === 40416) {
itemObj.name = getFruitName(id);
itemObj.type = 'produce';
produce.push(itemObj);
} else {
// 尝试识别为种子
const plantName = getPlantNameBySeedId(id);
if (plantName && plantName !== `种子${id}`) {
itemObj.name = plantName + '种子';
itemObj.type = 'seed';
seeds.push(itemObj);
} else {
// 过滤掉货币类物品和特殊物品
// 1001:金币, 1002:点券, 1101:种植经验, 3001:普通收藏点, 1011/1012:化肥容器
if ([1001, 1002, 1101, 3001, 1011, 1012].includes(id)) {
continue;
}
// 其他物品
itemObj.name = getItemNameById(id);
others.push(itemObj);
}
}
}
return { seeds, produce, others };
}
async sellAllFruits() {
if (!this.bot.network.connected) return;
try {
const bagReply = await this.getBag();
const items = this.getBagItems(bagReply);
const toSell = [];
const names = [];
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
const uid = item.uid ? toNum(item.uid) : 0;
if (this.isFruitIdBySeedData(id) && count > 0) {
if (uid === 0) continue; // 跳过无效格子
toSell.push(item);
names.push(`${getFruitName(id)} ${count}`);
}
}
if (toSell.length === 0) return;
const reply = await this.sellItems(toSell);
const totalGold = this.extractGold(reply);
log('仓库', `成功出售: ${names.join('')}。共获得 ${totalGold} 金币`);
emitRuntimeHint(false);
} catch (e) {
logWarn('仓库', `物品出售失败: ${e.message}`);
}
}
async useItems(items) {
if (!items || items.length === 0) return { success: true, message: '没有选择物品' };
const results = [];
const fertilizers = new Set([80001, 80002, 80003, 80004, 80011, 80012, 80013, 80014]);
const formatGains = (replyItems, excludeId) => {
if (!replyItems || replyItems.length === 0) return '';
const parts = [];
for (const replyItem of replyItems) {
const gainId = toNum(replyItem.id);
const gainCount = toNum(replyItem.count);
if (excludeId && gainId === excludeId) continue;
if (!gainId || !gainCount) continue;
parts.push(`${getItemNameById(gainId)} x${gainCount}`);
}
return parts.join('');
};
const buildGainList = (replyItems, excludeId) => {
if (!replyItems || replyItems.length === 0) return [];
const gains = [];
for (const replyItem of replyItems) {
const gainId = toNum(replyItem.id);
const gainCount = toNum(replyItem.count);
if (excludeId && gainId === excludeId) continue;
if (!gainId || !gainCount) continue;
gains.push({ id: gainId, count: gainCount });
}
return gains;
};
const buildItemCountMap = (bagItems) => {
const map = new Map();
for (const bagItem of bagItems) {
const bagId = toNum(bagItem.id);
const bagCount = toNum(bagItem.count);
if (!bagId || !bagCount) continue;
map.set(bagId, (map.get(bagId) || 0) + bagCount);
}
return map;
};
const diffGains = (beforeMap, afterMap, excludeId) => {
const parts = [];
for (const [id, afterCount] of afterMap.entries()) {
if (excludeId && id === excludeId) continue;
const beforeCount = beforeMap.get(id) || 0;
const delta = afterCount - beforeCount;
if (delta > 0) {
parts.push(`${getItemNameById(id)} x${delta}`);
}
}
return parts.join('');
};
const diffGainList = (beforeMap, afterMap, excludeId) => {
const gains = [];
for (const [id, afterCount] of afterMap.entries()) {
if (excludeId && id === excludeId) continue;
const beforeCount = beforeMap.get(id) || 0;
const delta = afterCount - beforeCount;
if (delta > 0) gains.push({ id, count: delta });
}
return gains;
};
const mergeGains = (gainMap, gains) => {
for (const gain of gains) {
if (!gain || !gain.id || !gain.count) continue;
gainMap.set(gain.id, (gainMap.get(gain.id) || 0) + gain.count);
}
};
const totalGains = new Map();
for (const item of items) {
const id = Number(item.id);
const count = Number(item.count);
let uid = item.uid != null ? Number(item.uid) : 0;
const name = getItemNameById(id);
try {
let usedCount = count;
let gainsText = '';
let gainList = [];
if (id === 100003) {
const beforeReply = await this.getBag();
const beforeItems = this.getBagItems(beforeReply);
const beforeMap = buildItemCountMap(beforeItems);
if (!uid) {
const bagReply = await this.getBag();
const bagItems = this.getBagItems(bagReply);
const found = bagItems.find(bagItem => toNum(bagItem.id) === id && toNum(bagItem.uid) > 0);
if (found) uid = toNum(found.uid);
}
if (!uid) {
throw new Error('礼包缺少UID');
}
const itemWriter = protobuf.Writer.create();
itemWriter.uint32(8).int64(toLong(id));
itemWriter.uint32(16).int64(toLong(count));
itemWriter.uint32(48).int64(toLong(uid));
const body = protobuf.Writer.create().uint32(10).bytes(itemWriter.finish()).finish();
await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'Use', body);
const afterReply = await this.getBag();
const afterItems = this.getBagItems(afterReply);
const afterMap = buildItemCountMap(afterItems);
gainsText = diffGains(beforeMap, afterMap, id);
gainList = diffGainList(beforeMap, afterMap, id);
} else {
const payload = {
items: [{
item_id: toLong(id),
count: toLong(count),
land_count: toLong(1)
}]
};
const body = types.BatchUseRequest.encode(types.BatchUseRequest.create(payload)).finish();
const { body: replyBody } = await this.bot.network.sendMsgAsync('gamepb.itempb.ItemService', 'BatchUse', body);
const reply = types.BatchUseReply.decode(replyBody);
gainsText = formatGains(reply.items, id);
gainList = buildGainList(reply.items, id);
}
if (fertilizers.has(id)) {
results.push(`${name}: 已使用(消耗 ${usedCount} 个)${gainsText ? `,获得:${gainsText}` : ''}`);
} else {
results.push(`${name}: 已使用${gainsText ? `,获得:${gainsText}` : ''}`);
}
if (gainsText) {
log('仓库', `使用 ${name} 获得:${gainsText}`);
}
mergeGains(totalGains, gainList);
} catch (e) {
logWarn('仓库', `物品使用失败 (${name}): ${e.message}`);
results.push(`${name}: 使用失败 - ${e.message}`);
}
}
const gains = [];
for (const [id, count] of totalGains.entries()) {
gains.push({ id, name: getItemNameById(id), count });
}
return { success: true, message: results.join('; '), gains };
}
startLoop(interval = 60000) {
if (this.sellTimer) return;
this.sellInterval = interval;
// 延迟启动,避免刚上线就请求
setTimeout(() => {
if (!this.bot.isRunning) return;
this.sellAllFruits();
this.sellTimer = setInterval(() => this.sellAllFruits(), this.sellInterval);
}, 10000);
}
stopLoop() {
if (this.sellTimer) {
clearInterval(this.sellTimer);
this.sellTimer = null;
}
}
}
module.exports = { WarehouseManager };

245
211/server/src/decode.js Normal file
View File

@@ -0,0 +1,245 @@
/**
* 解码/验证工具模式
*/
const protobuf = require('protobufjs');
const Long = require('long');
const { PHASE_NAMES } = require('./config');
const { types, getRoot } = require('./proto');
const { toNum } = require('./utils');
// ============ 辅助函数 ============
/** JSON.stringify replacer, 处理 Long 和 Buffer */
function longReplacer(key, value) {
if (value && typeof value === 'object' && value.low !== undefined && value.high !== undefined) {
return Long.fromBits(value.low, value.high, value.unsigned).toString();
}
if (value && value.type === 'Buffer' && Array.isArray(value.data)) {
return `<${value.data.length} bytes>`;
}
return value;
}
/** 尝试将 bytes 解码为 UTF-8 字符串 */
function tryDecodeString(bytes) {
try {
const str = Buffer.from(bytes).toString('utf8');
const printable = str.split('').filter(c => c.charCodeAt(0) >= 32 || c === '\n' || c === '\r' || c === '\t').length;
if (printable > str.length * 0.8 && str.length > 0) return str;
} catch (e) {}
return null;
}
/** 通用 protobuf 解码 (无 schema, 显示原始字段) */
function tryGenericDecode(buf) {
console.log('=== 通用 protobuf 解码 (无schema) ===');
try {
const reader = protobuf.Reader.create(buf);
while (reader.pos < reader.len) {
const tag = reader.uint32();
const fieldNum = tag >>> 3;
const wireType = tag & 7;
let value;
switch (wireType) {
case 0: value = reader.int64().toString(); console.log(` field ${fieldNum} (varint): ${value}`); break;
case 1: value = reader.fixed64().toString(); console.log(` field ${fieldNum} (fixed64): ${value}`); break;
case 2: {
const bytes = reader.bytes();
const str = tryDecodeString(bytes);
if (str !== null) {
console.log(` field ${fieldNum} (bytes/${bytes.length}): "${str}"`);
} else {
console.log(` field ${fieldNum} (bytes/${bytes.length}): ${Buffer.from(bytes).toString('hex')}`);
}
break;
}
case 5: value = reader.float(); console.log(` field ${fieldNum} (float): ${value}`); break;
default: console.log(` field ${fieldNum} (wire ${wireType}): <skip>`); reader.skipType(wireType); break;
}
}
} catch (e) {
console.log(` 解码中断: ${e.message}`);
}
}
// ============ 验证模式 ============
async function verifyMode() {
console.log('\n====== 验证模式 ======\n');
// Login Request
const loginB64 = 'CigKGWdhbWVwYi51c2VycGIuVXNlclNlcnZpY2USBUxvZ2luGAEgASgAEmEYACIAKjwKEDEuNi4wLjhfMjAyNTEyMjQSE1dpbmRvd3MgVW5rbm93biB4NjQqBHdpZmlQzL0BagltaWNyb3NvZnQwADoEMTI1NkIVCgASABoAIgAqBW90aGVyMAI6AEIA';
try {
const msg = types.GateMessage.decode(Buffer.from(loginB64, 'base64'));
console.log(`[OK] Login Request: ${msg.meta.service_name}.${msg.meta.method_name} seq=${msg.meta.client_seq}`);
const req = types.LoginRequest.decode(msg.body);
console.log(` device=${req.device_info?.client_version} scene=${req.scene_id}`);
} catch (e) { console.log(`[FAIL] Login Request: ${e.message}`); }
// AllLands Response
const allLandsB64 = 'ClwKG2dhbWVwYi5wbGFudHBiLlBsYW50U2VydmljZRIIQWxsTGFuZHMYAiAEKARCLQoJeC10cmFjZWlkEiBhOWZhNmZhZmYwZmI0ZDU5ZjQ5ZDJiZTJlYTY2NGU3NBK7BwpMCAEQARgBIARCDQgSEBwaBwjpBxDAmgxKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCNu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAIQARgBIARCDQgSEB0aBwjpBxCQoQ9KAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAMQARgBIARCDQgSEB4aBwjpBxDgpxJKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAQQARgBIARCDQgSEB8aBwjpBxCwrhVKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAUQARgBIARCDQgSECAaBwjpBxCAtRhKAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCNu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQpMCAYQARgBIARCDQgSECEaBwjpBxCgwh5KAFIuCOOgPhIJ6IOh6JCd5Y2cIgoIBhCOu5rMBhgTKAFQw7gCWAp4eIABAYgBAZABCoABAQoPCAcgBDoHCAYQBRiIJ2ABCg8ICCAEOgcIBxAHGJBOYAEKEAgJIAQ6CAgIEAkYoJwBYAEKDggKIAQ6CAgJEAsYsOoBCg4ICyAEOggIChANGMC4AgoOCAwgBDoICAsQDxjg1AMKDggNIAQ6CAgMEBEYgPEECg4IDiAEOggIDRATGKCNBgoOCA8gBDoICA4QFRjAqQcKDggQIAQ6CAgPEBcY4MUICg4IESAEOggIEBAZGIDiCQoOCBIgBDoICBEQGxig/goKDggTIAQ6CAgSEB0YwJoMCg4IFCAEOggIExAfGOC2DQoOCBUgBDoICBQQIRiA0w4KDggWIAQ6CAgVECMYoO8PCg4IFyAEOggIFhAlGMCLEQoOCBggBDoICBcQJxjgpxISCQicThj/k+vcAxIJCJ1OGP+T69wDEgkInk4Y/5Pr3AMSCwiRThAJGP+T69wDEg0IlE4YZCCTTihkOJNOEhAIlU4QBxj/k+vcAygXOJVOEhAIlk4QCxj/k+vcAygLOJVOEhAIl04QBRj/k+vcAygFOJVOEgkImE4Y/5Pr3AMSDQiZThAMGP+T69wDKAwSCwiaThABGP+T69wDEg0Ikk4QCRj/k+vcAygJEg0Ik04YZCCTTihkOJNOEgkIm04Y/5Pr3AM=';
try {
const msg = types.GateMessage.decode(Buffer.from(allLandsB64, 'base64'));
const reply = types.AllLandsReply.decode(msg.body);
console.log(`[OK] AllLands Reply: ${reply.lands.length} 块土地`);
for (const land of reply.lands.slice(0, 3)) {
const id = toNum(land.id);
const unlocked = land.unlocked;
const plantName = land.plant?.name || '空';
const phases = land.plant?.phases || [];
const lastPhase = phases.length > 0 ? phases[phases.length - 1].phase : -1;
console.log(` 土地#${id}: ${unlocked ? '已解锁' : '未解锁'} 植物=${plantName} 阶段=${PHASE_NAMES[lastPhase] || lastPhase}`);
}
if (reply.lands.length > 3) console.log(` ... 还有 ${reply.lands.length - 3}`);
} catch (e) { console.log(`[FAIL] AllLands Reply: ${e.message}`); }
// Harvest Request
const harvestB64 = 'CiwKG2dhbWVwYi5wbGFudHBiLlBsYW50U2VydmljZRIHSGFydmVzdBgBIBsoGhIQCgYBAgMEBQYQyOHR8gMYAQ==';
try {
const msg = types.GateMessage.decode(Buffer.from(harvestB64, 'base64'));
const req = types.HarvestRequest.decode(msg.body);
console.log(`[OK] Harvest Request: land_ids=[${req.land_ids.join(',')}] host_gid=${req.host_gid} is_all=${req.is_all}`);
} catch (e) { console.log(`[FAIL] Harvest Request: ${e.message}`); }
console.log('\n====== 验证完成 ======\n');
}
// ============ 解码模式 ============
async function decodeMode(args) {
let inputData = '';
let typeName = '';
let isHex = false;
let isGateWrapped = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--decode') continue;
if (args[i] === '--type' && args[i + 1]) { typeName = args[++i]; continue; }
if (args[i] === '--hex') { isHex = true; continue; }
if (args[i] === '--gate') { isGateWrapped = true; continue; }
if (!inputData) inputData = args[i];
}
if (!inputData) {
console.log(`
PB数据解码工具
==============
用法:
node client.js --decode <base64数据>
node client.js --decode <hex数据> --hex
node client.js --decode <base64数据> --type <消息类型>
node client.js --decode <base64数据> --gate
参数:
<数据> base64编码的pb数据 (默认), 或hex编码 (配合 --hex)
--hex 输入数据为hex编码
--gate 外层是 gatepb.Message 包装, 自动解析 meta + body
--type 指定消息类型, 如: gatepb.Message, gamepb.plantpb.AllLandsReply 等
可用类型:
gatepb.Message / gatepb.Meta
gamepb.userpb.LoginRequest / LoginReply / HeartbeatRequest / HeartbeatReply
gamepb.plantpb.AllLandsRequest / AllLandsReply / HarvestRequest / HarvestReply
gamepb.plantpb.WaterLandRequest / WeedOutRequest / InsecticideRequest
gamepb.plantpb.PlantRequest / PlantReply / RemovePlantRequest / RemovePlantReply
gamepb.shoppb.ShopInfoRequest / ShopInfoReply / BuyGoodsRequest / BuyGoodsReply
gamepb.friendpb.GetAllRequest / GetAllReply / GameFriend
示例:
node client.js --decode CigKGWdhbWVwYi... --gate
node client.js --decode 0a1c0a19... --hex --type gatepb.Message
`);
return;
}
const root = getRoot();
let buf;
try {
buf = isHex ? Buffer.from(inputData, 'hex') : Buffer.from(inputData, 'base64');
} catch (e) {
console.error(`输入数据解码失败: ${e.message}`);
return;
}
console.log(`数据长度: ${buf.length} 字节\n`);
// --gate: 先解析外层 gatepb.Message
if (isGateWrapped) {
try {
const msg = types.GateMessage.decode(buf);
const meta = msg.meta;
console.log('=== gatepb.Message (外层) ===');
console.log(` service: ${meta.service_name}`);
console.log(` method: ${meta.method_name}`);
console.log(` type: ${meta.message_type} (${meta.message_type === 1 ? 'Request' : meta.message_type === 2 ? 'Response' : 'Notify'})`);
console.log(` client_seq: ${meta.client_seq}`);
console.log(` server_seq: ${meta.server_seq}`);
if (toNum(meta.error_code) !== 0) {
console.log(` error_code: ${meta.error_code}`);
console.log(` error_msg: ${meta.error_message}`);
}
console.log('');
if (msg.body && msg.body.length > 0) {
const svc = meta.service_name || '';
const mtd = meta.method_name || '';
const isReq = meta.message_type === 1;
const suffix = isReq ? 'Request' : 'Reply';
const autoType = `${svc.replace('Service', '')}.${mtd}${suffix}`;
let bodyType = null;
try { bodyType = root.lookupType(autoType); } catch (e) {}
if (!bodyType) {
const parts = svc.split('.');
if (parts.length >= 2) {
const ns = parts.slice(0, parts.length - 1).join('.');
try { bodyType = root.lookupType(`${ns}.${mtd}${suffix}`); } catch (e) {}
}
}
if (bodyType) {
console.log(`=== ${bodyType.fullName} (body 自动推断) ===`);
const decoded = bodyType.decode(msg.body);
console.log(JSON.stringify(decoded.toJSON(), longReplacer, 2));
} else {
console.log(`=== body (未能自动推断类型, 用 --type 手动指定 body 类型) ===`);
console.log(` hex: ${Buffer.from(msg.body).toString('hex')}`);
console.log(` base64: ${Buffer.from(msg.body).toString('base64')}`);
tryGenericDecode(msg.body);
}
}
} catch (e) {
console.error(`gatepb.Message 解码失败: ${e.message}`);
}
return;
}
// --type: 指定类型解码
if (typeName) {
try {
const msgType = root.lookupType(typeName);
const decoded = msgType.decode(buf);
console.log(`=== ${typeName} ===`);
console.log(JSON.stringify(decoded.toJSON(), longReplacer, 2));
} catch (e) {
console.error(`解码失败 (${typeName}): ${e.message}`);
}
return;
}
// 未指定类型,自动尝试
console.log('未指定类型,自动尝试...\n');
try {
const msg = types.GateMessage.decode(buf);
if (msg.meta && (msg.meta.service_name || msg.meta.method_name)) {
console.log('=== 检测为 gatepb.Message ===');
console.log(JSON.stringify(msg.toJSON(), longReplacer, 2));
return;
}
} catch (e) {}
tryGenericDecode(buf);
}
module.exports = { verifyMode, decodeMode };

648
211/server/src/farm.js Normal file
View File

@@ -0,0 +1,648 @@
/**
* 自己的农场操作 - 收获/浇水/除草/除虫/铲除/种植/商店/巡田
*/
const protobuf = require('protobufjs');
const { CONFIG, PlantPhase, PHASE_NAMES } = require('./config');
const { types } = require('./proto');
const { sendMsgAsync, getUserState, networkEvents } = require('./network');
const { toLong, toNum, getServerTimeSec, toTimeSec, log, logWarn, sleep } = require('./utils');
const { getPlantNameBySeedId, getPlantName, getPlantExp, formatGrowTime, getPlantGrowTime } = require('./gameConfig');
const { getPlantingRecommendation } = require('../tools/calc-exp-yield');
// ============ 内部状态 ============
let isCheckingFarm = false;
let isFirstFarmCheck = true;
let farmCheckTimer = null;
let farmLoopRunning = false;
// ============ 农场 API ============
// 操作限制更新回调 (由 friend.js 设置)
let onOperationLimitsUpdate = null;
function setOperationLimitsCallback(callback) {
onOperationLimitsUpdate = callback;
}
async function getAllLands() {
const body = types.AllLandsRequest.encode(types.AllLandsRequest.create({})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'AllLands', body);
const reply = types.AllLandsReply.decode(replyBody);
// 更新操作限制
if (reply.operation_limits && onOperationLimitsUpdate) {
onOperationLimitsUpdate(reply.operation_limits);
}
return reply;
}
async function harvest(landIds) {
const state = getUserState();
const body = types.HarvestRequest.encode(types.HarvestRequest.create({
land_ids: landIds,
host_gid: toLong(state.gid),
is_all: true,
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body);
return types.HarvestReply.decode(replyBody);
}
async function waterLand(landIds) {
const state = getUserState();
const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({
land_ids: landIds,
host_gid: toLong(state.gid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body);
return types.WaterLandReply.decode(replyBody);
}
async function weedOut(landIds) {
const state = getUserState();
const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({
land_ids: landIds,
host_gid: toLong(state.gid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body);
return types.WeedOutReply.decode(replyBody);
}
async function insecticide(landIds) {
const state = getUserState();
const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({
land_ids: landIds,
host_gid: toLong(state.gid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body);
return types.InsecticideReply.decode(replyBody);
}
// 普通肥料 ID
const NORMAL_FERTILIZER_ID = 1011;
/**
* 施肥 - 必须逐块进行,服务器不支持批量
* 游戏中拖动施肥间隔很短,这里用 50ms
*/
async function fertilize(landIds, fertilizerId = NORMAL_FERTILIZER_ID) {
let successCount = 0;
for (const landId of landIds) {
try {
const body = types.FertilizeRequest.encode(types.FertilizeRequest.create({
land_ids: [toLong(landId)],
fertilizer_id: toLong(fertilizerId),
})).finish();
await sendMsgAsync('gamepb.plantpb.PlantService', 'Fertilize', body);
successCount++;
} catch (e) {
// 施肥失败(可能肥料不足),停止继续
break;
}
if (landIds.length > 1) await sleep(50); // 50ms 间隔
}
return successCount;
}
async function removePlant(landIds) {
const body = types.RemovePlantRequest.encode(types.RemovePlantRequest.create({
land_ids: landIds.map(id => toLong(id)),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'RemovePlant', body);
return types.RemovePlantReply.decode(replyBody);
}
// ============ 商店 API ============
async function getShopInfo(shopId) {
const body = types.ShopInfoRequest.encode(types.ShopInfoRequest.create({
shop_id: toLong(shopId),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.shoppb.ShopService', 'ShopInfo', body);
return types.ShopInfoReply.decode(replyBody);
}
async function buyGoods(goodsId, num, price) {
const body = types.BuyGoodsRequest.encode(types.BuyGoodsRequest.create({
goods_id: toLong(goodsId),
num: toLong(num),
price: toLong(price),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.shoppb.ShopService', 'BuyGoods', body);
return types.BuyGoodsReply.decode(replyBody);
}
// ============ 种植 ============
const plantNextAllowed = new Map();
const PLANT_COOLDOWN_MS = 5000;
const PLANT_ALREADY_COOLDOWN_MS = 30000;
function encodePlantRequest(seedId, landIds) {
const writer = protobuf.Writer.create();
const itemWriter = writer.uint32(18).fork();
itemWriter.uint32(8).int64(seedId);
const idsWriter = itemWriter.uint32(18).fork();
for (const id of landIds) {
idsWriter.int64(id);
}
idsWriter.ldelim();
itemWriter.ldelim();
return writer.finish();
}
/**
* 种植 - 游戏中拖动种植间隔很短,这里用 50ms
*/
async function plantSeeds(seedId, landIds) {
let successCount = 0;
const now = Date.now();
for (const landId of landIds) {
const nextAllowed = plantNextAllowed.get(landId) || 0;
if (now < nextAllowed) continue;
plantNextAllowed.set(landId, now + PLANT_COOLDOWN_MS);
try {
const body = encodePlantRequest(seedId, [landId]);
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Plant', body);
types.PlantReply.decode(replyBody);
successCount++;
} catch (e) {
const msg = String(e && e.message ? e.message : e);
if (msg.includes('1001008') || msg.includes('土地已种植')) {
plantNextAllowed.set(landId, now + PLANT_ALREADY_COOLDOWN_MS);
log('种植', `土地#${landId} 已种植,跳过`);
} else {
logWarn('种植', `土地#${landId} 失败: ${msg}`);
}
}
if (landIds.length > 1) await sleep(50); // 50ms 间隔
}
return successCount;
}
async function findBestSeed(landsCount) {
const SEED_SHOP_ID = 2;
const shopReply = await getShopInfo(SEED_SHOP_ID);
if (!shopReply.goods_list || shopReply.goods_list.length === 0) {
logWarn('商店', '种子商店无商品');
return null;
}
const state = getUserState();
const available = [];
for (const goods of shopReply.goods_list) {
if (!goods.unlocked) continue;
let meetsConditions = true;
let requiredLevel = 0;
const conds = goods.conds || [];
for (const cond of conds) {
if (toNum(cond.type) === 1) {
requiredLevel = toNum(cond.param);
if (state.level < requiredLevel) {
meetsConditions = false;
break;
}
}
}
if (!meetsConditions) continue;
const limitCount = toNum(goods.limit_count);
const boughtNum = toNum(goods.bought_num);
if (limitCount > 0 && boughtNum >= limitCount) continue;
available.push({
goods,
goodsId: toNum(goods.id),
seedId: toNum(goods.item_id),
price: toNum(goods.price),
requiredLevel,
});
}
if (available.length === 0) {
logWarn('商店', '没有可购买的种子');
return null;
}
if (CONFIG.forceLowestLevelCrop) {
available.sort((a, b) => a.requiredLevel - b.requiredLevel || a.price - b.price);
return available[0];
}
try {
log('商店', `等级: ${state.level},土地数量: ${landsCount}`);
const rec = getPlantingRecommendation(state.level, landsCount == null ? 18 : landsCount, { top: 50 });
const rankedSeedIds = rec.candidatesNoFert.map(x => x.seedId);
for (const seedId of rankedSeedIds) {
const hit = available.find(x => x.seedId === seedId);
if (hit) return hit;
}
} catch (e) {
logWarn('商店', `经验效率推荐失败,使用兜底策略: ${e.message}`);
}
// 兜底等级在28级以前还是白萝卜比较好28级以上选最高等级的种子
if(state.level && state.level <= 28){
available.sort((a, b) => a.requiredLevel - b.requiredLevel);
}else{
available.sort((a, b) => b.requiredLevel - a.requiredLevel);
}
return available[0];
}
async function autoPlantEmptyLands(deadLandIds, emptyLandIds, unlockedLandCount) {
let landsToPlant = emptyLandIds.length > 0 ? [...emptyLandIds] : [];
const state = getUserState();
// 1. 铲除枯死/收获残留植物(一键操作)
if (deadLandIds.length > 0) {
try {
await removePlant(deadLandIds);
log('铲除', `已铲除 ${deadLandIds.length} 块 (${deadLandIds.join(',')})`);
landsToPlant.push(...deadLandIds);
} catch (e) {
logWarn('铲除', `批量铲除失败: ${e.message}`);
// 失败时仍然尝试种植
landsToPlant.push(...deadLandIds);
}
}
if (landsToPlant.length === 0) return;
if (landsToPlant.length > 1) {
landsToPlant = Array.from(new Set(landsToPlant));
}
if (landsToPlant.length > 0) {
const now = Date.now();
const filtered = [];
for (const landId of landsToPlant) {
const nextAllowed = plantNextAllowed.get(landId) || 0;
if (now >= nextAllowed) {
filtered.push(landId);
}
}
landsToPlant = filtered;
}
if (landsToPlant.length === 0) return;
// 2. 查询种子商店
let bestSeed;
try {
bestSeed = await findBestSeed(unlockedLandCount);
} catch (e) {
logWarn('商店', `查询失败: ${e.message}`);
return;
}
if (!bestSeed) return;
const seedName = getPlantNameBySeedId(bestSeed.seedId);
const growTime = getPlantGrowTime(1020000 + (bestSeed.seedId - 20000)); // 转换为植物ID
const growTimeStr = growTime > 0 ? ` 生长${formatGrowTime(growTime)}` : '';
log('商店', `最佳种子: ${seedName} (${bestSeed.seedId}) 价格=${bestSeed.price}金币${growTimeStr}`);
// 3. 购买
const needCount = landsToPlant.length;
const totalCost = bestSeed.price * needCount;
if (totalCost > state.gold) {
logWarn('商店', `金币不足! 需要 ${totalCost} 金币, 当前 ${state.gold} 金币`);
const canBuy = Math.floor(state.gold / bestSeed.price);
if (canBuy <= 0) return;
landsToPlant = landsToPlant.slice(0, canBuy);
log('商店', `金币有限,只种 ${canBuy} 块地`);
}
let actualSeedId = bestSeed.seedId;
try {
const buyReply = await buyGoods(bestSeed.goodsId, landsToPlant.length, bestSeed.price);
if (buyReply.get_items && buyReply.get_items.length > 0) {
const gotItem = buyReply.get_items[0];
const gotId = toNum(gotItem.id);
const gotCount = toNum(gotItem.count);
log('购买', `获得物品: id=${gotId} count=${gotCount}`);
if (gotId > 0) actualSeedId = gotId;
}
if (buyReply.cost_items) {
for (const item of buyReply.cost_items) {
state.gold -= toNum(item.count);
}
}
const boughtName = getPlantNameBySeedId(actualSeedId);
log('购买', `已购买 ${boughtName}种子 x${landsToPlant.length}, 花费 ${bestSeed.price * landsToPlant.length} 金币`);
} catch (e) {
logWarn('购买', e.message);
return;
}
// 4. 种植逐块拖动间隔50ms
let plantedLands = [];
try {
const planted = await plantSeeds(actualSeedId, landsToPlant);
log('种植', `已在 ${planted} 块地种植 (${landsToPlant.join(',')})`);
if (planted > 0) {
plantedLands = landsToPlant.slice(0, planted);
}
} catch (e) {
logWarn('种植', e.message);
}
}
// ============ 土地分析 ============
/**
* 根据服务器时间确定当前实际生长阶段
*/
function getCurrentPhase(phases, debug, landLabel) {
if (!phases || phases.length === 0) return null;
const nowSec = getServerTimeSec();
if (debug) {
console.log(` ${landLabel} 服务器时间=${nowSec} (${new Date(nowSec * 1000).toLocaleTimeString()})`);
for (let i = 0; i < phases.length; i++) {
const p = phases[i];
const bt = toTimeSec(p.begin_time);
const phaseName = PHASE_NAMES[p.phase] || `阶段${p.phase}`;
const diff = bt > 0 ? (bt - nowSec) : 0;
const diffStr = diff > 0 ? `(未来 ${diff}s)` : diff < 0 ? `(已过 ${-diff}s)` : '';
console.log(` ${landLabel} [${i}] ${phaseName}(${p.phase}) begin=${bt} ${diffStr} dry=${toTimeSec(p.dry_time)} weed=${toTimeSec(p.weeds_time)} insect=${toTimeSec(p.insect_time)}`);
}
}
for (let i = phases.length - 1; i >= 0; i--) {
const beginTime = toTimeSec(phases[i].begin_time);
if (beginTime > 0 && beginTime <= nowSec) {
if (debug) {
console.log(` ${landLabel} → 当前阶段: ${PHASE_NAMES[phases[i].phase] || phases[i].phase}`);
}
return phases[i];
}
}
if (debug) {
console.log(` ${landLabel} → 所有阶段都在未来,使用第一个: ${PHASE_NAMES[phases[0].phase] || phases[0].phase}`);
}
return phases[0];
}
function analyzeLands(lands) {
const result = {
harvestable: [], needWater: [], needWeed: [], needBug: [],
growing: [], empty: [], dead: [],
harvestableInfo: [], // 收获植物的详细信息 { id, name, exp }
};
const nowSec = getServerTimeSec();
const debug = false;
if (debug) {
console.log('');
console.log('========== 首次巡田详细日志 ==========');
console.log(` 服务器时间(秒): ${nowSec} (${new Date(nowSec * 1000).toLocaleString()})`);
console.log(` 总土地数: ${lands.length}`);
console.log('');
}
for (const land of lands) {
const id = toNum(land.id);
if (!land.unlocked) {
if (debug) console.log(` 土地#${id}: 未解锁`);
continue;
}
const plant = land.plant;
if (!plant) {
result.empty.push(id);
if (debug) console.log(` 土地#${id}: 空地`);
continue;
}
if (!plant.phases || plant.phases.length === 0) {
const plantId = toNum(plant.id);
if (plantId > 0 || plant.name) {
result.growing.push(id);
if (debug) console.log(` 土地#${id}: 生长中(阶段未知)`);
} else {
result.empty.push(id);
if (debug) console.log(` 土地#${id}: 空地`);
}
continue;
}
const plantName = plant.name || '未知作物';
const landLabel = `土地#${id}(${plantName})`;
if (debug) {
console.log(` ${landLabel}: phases=${plant.phases.length} dry_num=${toNum(plant.dry_num)} weed_owners=${(plant.weed_owners||[]).length} insect_owners=${(plant.insect_owners||[]).length}`);
}
const currentPhase = getCurrentPhase(plant.phases, debug, landLabel);
if (!currentPhase) {
result.empty.push(id);
continue;
}
const phaseVal = currentPhase.phase;
if (phaseVal === PlantPhase.DEAD) {
result.dead.push(id);
if (debug) console.log(` → 结果: 枯死`);
continue;
}
if (phaseVal === PlantPhase.MATURE) {
result.harvestable.push(id);
// 收集植物信息用于日志
const plantId = toNum(plant.id);
const plantNameFromConfig = getPlantName(plantId);
const plantExp = getPlantExp(plantId);
result.harvestableInfo.push({
landId: id,
plantId,
name: plantNameFromConfig || plantName,
exp: plantExp,
});
if (debug) console.log(` → 结果: 可收获 (${plantNameFromConfig} +${plantExp}经验)`);
continue;
}
let landNeeds = [];
const dryNum = toNum(plant.dry_num);
const dryTime = toTimeSec(currentPhase.dry_time);
if (dryNum > 0 || (dryTime > 0 && dryTime <= nowSec)) {
result.needWater.push(id);
landNeeds.push('缺水');
}
const weedsTime = toTimeSec(currentPhase.weeds_time);
const hasWeeds = (plant.weed_owners && plant.weed_owners.length > 0) || (weedsTime > 0 && weedsTime <= nowSec);
if (hasWeeds) {
result.needWeed.push(id);
landNeeds.push('有草');
}
const insectTime = toTimeSec(currentPhase.insect_time);
const hasBugs = (plant.insect_owners && plant.insect_owners.length > 0) || (insectTime > 0 && insectTime <= nowSec);
if (hasBugs) {
result.needBug.push(id);
landNeeds.push('有虫');
}
result.growing.push(id);
if (debug) {
const needStr = landNeeds.length > 0 ? ` 需要: ${landNeeds.join(',')}` : '';
console.log(` → 结果: 生长中(${PHASE_NAMES[phaseVal] || phaseVal})${needStr}`);
}
}
if (debug) {
console.log('');
console.log('========== 巡田分析汇总 ==========');
console.log(` 可收获: ${result.harvestable.length} [${result.harvestable.join(',')}]`);
console.log(` 生长中: ${result.growing.length} [${result.growing.join(',')}]`);
console.log(` 缺水: ${result.needWater.length} [${result.needWater.join(',')}]`);
console.log(` 有草: ${result.needWeed.length} [${result.needWeed.join(',')}]`);
console.log(` 有虫: ${result.needBug.length} [${result.needBug.join(',')}]`);
console.log(` 空地: ${result.empty.length} [${result.empty.join(',')}]`);
console.log(` 枯死: ${result.dead.length} [${result.dead.join(',')}]`);
console.log('====================================');
console.log('');
}
return result;
}
// ============ 巡田主循环 ============
async function checkFarm() {
const state = getUserState();
if (isCheckingFarm || !state.gid) return;
isCheckingFarm = true;
try {
const landsReply = await getAllLands();
if (!landsReply.lands || landsReply.lands.length === 0) {
log('农场', '没有土地数据');
return;
}
const lands = landsReply.lands;
const status = analyzeLands(lands);
const unlockedLandCount = lands.filter(land => land && land.unlocked).length;
isFirstFarmCheck = false;
// 构建状态摘要
const statusParts = [];
if (status.harvestable.length) statusParts.push(`收:${status.harvestable.length}`);
if (status.needWeed.length) statusParts.push(`草:${status.needWeed.length}`);
if (status.needBug.length) statusParts.push(`虫:${status.needBug.length}`);
if (status.needWater.length) statusParts.push(`水:${status.needWater.length}`);
if (status.dead.length) statusParts.push(`枯:${status.dead.length}`);
if (status.empty.length) statusParts.push(`空:${status.empty.length}`);
statusParts.push(`长:${status.growing.length}`);
const hasWork = status.harvestable.length || status.needWeed.length || status.needBug.length
|| status.needWater.length || status.dead.length || status.empty.length;
// 执行操作并收集结果
const actions = [];
// 一键操作:除草、除虫、浇水可以并行执行(游戏中都是一键完成)
const batchOps = [];
if (status.needWeed.length > 0) {
batchOps.push(weedOut(status.needWeed).then(() => actions.push(`除草${status.needWeed.length}`)).catch(e => logWarn('除草', e.message)));
}
if (status.needBug.length > 0) {
batchOps.push(insecticide(status.needBug).then(() => actions.push(`除虫${status.needBug.length}`)).catch(e => logWarn('除虫', e.message)));
}
if (status.needWater.length > 0) {
batchOps.push(waterLand(status.needWater).then(() => actions.push(`浇水${status.needWater.length}`)).catch(e => logWarn('浇水', e.message)));
}
if (batchOps.length > 0) {
await Promise.all(batchOps);
}
// 收获(一键操作)
let harvestedLandIds = [];
if (status.harvestable.length > 0) {
try {
await harvest(status.harvestable);
actions.push(`收获${status.harvestable.length}`);
harvestedLandIds = [...status.harvestable];
} catch (e) { logWarn('收获', e.message); }
}
// 铲除 + 种植 + 施肥(需要顺序执行)
const allDeadLands = [...status.dead, ...harvestedLandIds];
const allEmptyLands = [...status.empty];
if (allDeadLands.length > 0 || allEmptyLands.length > 0) {
try {
await autoPlantEmptyLands(allDeadLands, allEmptyLands, unlockedLandCount);
actions.push(`种植${allDeadLands.length + allEmptyLands.length}`);
} catch (e) { logWarn('种植', e.message); }
}
// 输出一行日志
const actionStr = actions.length > 0 ? `${actions.join('/')}` : '';
if(hasWork) {
log('农场', `[${statusParts.join(' ')}]${actionStr}${!hasWork ? ' 无需操作' : ''}`)
}
} catch (err) {
logWarn('巡田', `检查失败: ${err.message}`);
} finally {
isCheckingFarm = false;
}
}
/**
* 农场巡查循环 - 本次完成后等待指定秒数再开始下次
*/
async function farmCheckLoop() {
while (farmLoopRunning) {
await checkFarm();
if (!farmLoopRunning) break;
await sleep(CONFIG.farmCheckInterval);
}
}
function startFarmCheckLoop() {
if (farmLoopRunning) return;
farmLoopRunning = true;
// 监听服务器推送的土地变化事件
networkEvents.on('landsChanged', onLandsChangedPush);
// 延迟 2 秒后启动循环
farmCheckTimer = setTimeout(() => farmCheckLoop(), 2000);
}
/**
* 处理服务器推送的土地变化
*/
let lastPushTime = 0;
function onLandsChangedPush(lands) {
if (isCheckingFarm) return;
const now = Date.now();
if (now - lastPushTime < 500) return; // 500ms 防抖
lastPushTime = now;
log('农场', `收到推送: ${lands.length}块土地变化,检查中...`);
setTimeout(async () => {
if (!isCheckingFarm) {
await checkFarm();
}
}, 100);
}
function stopFarmCheckLoop() {
farmLoopRunning = false;
if (farmCheckTimer) { clearTimeout(farmCheckTimer); farmCheckTimer = null; }
networkEvents.removeListener('landsChanged', onLandsChangedPush);
}
module.exports = {
checkFarm, startFarmCheckLoop, stopFarmCheckLoop,
getCurrentPhase,
setOperationLimitsCallback,
};

650
211/server/src/friend.js Normal file
View File

@@ -0,0 +1,650 @@
/**
* 好友农场操作 - 进入/离开/帮忙/偷菜/巡查
*/
const { CONFIG, PlantPhase, PHASE_NAMES } = require('./config');
const { types } = require('./proto');
const { sendMsgAsync, getUserState, networkEvents } = require('./network');
const { toLong, toNum, getServerTimeSec, log, logWarn, sleep } = require('./utils');
const { getCurrentPhase, setOperationLimitsCallback } = require('./farm');
const { getPlantName } = require('./gameConfig');
// ============ 内部状态 ============
let isCheckingFriends = false;
let isFirstFriendCheck = true;
let friendCheckTimer = null;
let friendLoopRunning = false;
let lastResetDate = ''; // 上次重置日期 (YYYY-MM-DD)
// 操作限制状态 (从服务器响应中更新)
// 操作类型ID (根据游戏代码):
// 10001 = 收获, 10002 = 铲除, 10003 = 放草, 10004 = 放虫
// 10005 = 除草(帮好友), 10006 = 除虫(帮好友), 10007 = 浇水(帮好友), 10008 = 偷菜
const operationLimits = new Map();
// 操作类型名称映射
const OP_NAMES = {
10001: '收获',
10002: '铲除',
10003: '放草',
10004: '放虫',
10005: '除草',
10006: '除虫',
10007: '浇水',
10008: '偷菜',
};
// 配置: 是否只在有经验时才帮助好友
const HELP_ONLY_WITH_EXP = true; // !!!无效,暂时无法判断。有修复方法但是暂时没打算更新出来
// 配置: 是否启用放虫放草功能
const ENABLE_PUT_BAD_THINGS = false; // 无效!!!开启后会多次访问朋友导致被拉黑 请勿更改暂时关闭放虫放草功能
// ============ 好友 API ============
async function getAllFriends() {
const body = types.GetAllFriendsRequest.encode(types.GetAllFriendsRequest.create({})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'GetAll', body);
return types.GetAllFriendsReply.decode(replyBody);
}
// ============ 好友申请 API (微信同玩) ============
async function getApplications() {
const body = types.GetApplicationsRequest.encode(types.GetApplicationsRequest.create({})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'GetApplications', body);
return types.GetApplicationsReply.decode(replyBody);
}
async function acceptFriends(gids) {
const body = types.AcceptFriendsRequest.encode(types.AcceptFriendsRequest.create({
friend_gids: gids.map(g => toLong(g)),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.friendpb.FriendService', 'AcceptFriends', body);
return types.AcceptFriendsReply.decode(replyBody);
}
async function enterFriendFarm(friendGid) {
const body = types.VisitEnterRequest.encode(types.VisitEnterRequest.create({
host_gid: toLong(friendGid),
reason: 2, // ENTER_REASON_FRIEND
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.visitpb.VisitService', 'Enter', body);
return types.VisitEnterReply.decode(replyBody);
}
async function leaveFriendFarm(friendGid) {
const body = types.VisitLeaveRequest.encode(types.VisitLeaveRequest.create({
host_gid: toLong(friendGid),
})).finish();
try {
await sendMsgAsync('gamepb.visitpb.VisitService', 'Leave', body);
} catch (e) { /* 离开失败不影响主流程 */ }
}
/**
* 检查是否需要重置每日限制 (0点刷新)
*/
function checkDailyReset() {
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
if (lastResetDate !== today) {
if (lastResetDate !== '') {
log('系统', '跨日重置,清空操作限制缓存');
}
operationLimits.clear();
lastResetDate = today;
}
}
/**
* 更新操作限制状态
*/
function updateOperationLimits(limits) {
if (!limits || limits.length === 0) return;
checkDailyReset();
for (const limit of limits) {
const id = toNum(limit.id);
if (id > 0) {
const data = {
dayTimes: toNum(limit.day_times),
dayTimesLimit: toNum(limit.day_times_lt),
dayExpTimes: toNum(limit.day_exp_times),
dayExpTimesLimit: toNum(limit.day_ex_times_lt), // 注意: 字段名是 day_ex_times_lt (少个p)
};
operationLimits.set(id, data);
}
}
}
/**
* 检查某操作是否还能获得经验
*/
function canGetExp(opId) {
const limit = operationLimits.get(opId);
if (!limit) return false; // 没有限制信息,保守起见不帮助(等待农场检查获取限制)
if (limit.dayExpTimesLimit <= 0) return true; // 没有经验上限
return limit.dayExpTimes < limit.dayExpTimesLimit;
}
/**
* 检查某操作是否还有次数
*/
function canOperate(opId) {
const limit = operationLimits.get(opId);
if (!limit) return true;
if (limit.dayTimesLimit <= 0) return true;
return limit.dayTimes < limit.dayTimesLimit;
}
/**
* 获取某操作剩余次数
*/
function getRemainingTimes(opId) {
const limit = operationLimits.get(opId);
if (!limit || limit.dayTimesLimit <= 0) return 999;
return Math.max(0, limit.dayTimesLimit - limit.dayTimes);
}
/**
* 获取操作限制摘要 (用于日志显示)
*/
function getOperationLimitsSummary() {
const parts = [];
// 帮助好友操作 (10005=除草, 10006=除虫, 10007=浇水, 10008=偷菜)
for (const id of [10005, 10006, 10007, 10008]) {
const limit = operationLimits.get(id);
if (limit && limit.dayExpTimesLimit > 0) {
const name = OP_NAMES[id] || `#${id}`;
const expLeft = limit.dayExpTimesLimit - limit.dayExpTimes;
parts.push(`${name}${expLeft}/${limit.dayExpTimesLimit}`);
}
}
// 捣乱操作 (10003=放草, 10004=放虫)
for (const id of [10003, 10004]) {
const limit = operationLimits.get(id);
if (limit && limit.dayTimesLimit > 0) {
const name = OP_NAMES[id] || `#${id}`;
const left = limit.dayTimesLimit - limit.dayTimes;
parts.push(`${name}${left}/${limit.dayTimesLimit}`);
}
}
return parts;
}
async function helpWater(friendGid, landIds) {
const body = types.WaterLandRequest.encode(types.WaterLandRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WaterLand', body);
const reply = types.WaterLandReply.decode(replyBody);
updateOperationLimits(reply.operation_limits);
return reply;
}
async function helpWeed(friendGid, landIds) {
const body = types.WeedOutRequest.encode(types.WeedOutRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'WeedOut', body);
const reply = types.WeedOutReply.decode(replyBody);
updateOperationLimits(reply.operation_limits);
return reply;
}
async function helpInsecticide(friendGid, landIds) {
const body = types.InsecticideRequest.encode(types.InsecticideRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Insecticide', body);
const reply = types.InsecticideReply.decode(replyBody);
updateOperationLimits(reply.operation_limits);
return reply;
}
async function stealHarvest(friendGid, landIds) {
const body = types.HarvestRequest.encode(types.HarvestRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
is_all: true,
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'Harvest', body);
const reply = types.HarvestReply.decode(replyBody);
updateOperationLimits(reply.operation_limits);
return reply;
}
async function putInsects(friendGid, landIds) {
const body = types.PutInsectsRequest.encode(types.PutInsectsRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'PutInsects', body);
const reply = types.PutInsectsReply.decode(replyBody);
updateOperationLimits(reply.operation_limits);
return reply;
}
async function putWeeds(friendGid, landIds) {
const body = types.PutWeedsRequest.encode(types.PutWeedsRequest.create({
land_ids: landIds,
host_gid: toLong(friendGid),
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.plantpb.PlantService', 'PutWeeds', body);
const reply = types.PutWeedsReply.decode(replyBody);
updateOperationLimits(reply.operation_limits);
return reply;
}
// ============ 好友土地分析 ============
// 调试开关 - 设为好友名字可只查看该好友的土地分析详情,设为 true 查看全部false 关闭
const DEBUG_FRIEND_LANDS = false;
function analyzeFriendLands(lands, myGid, friendName = '') {
const result = {
stealable: [], // 可偷
stealableInfo: [], // 可偷植物信息 { landId, plantId, name }
needWater: [], // 需要浇水
needWeed: [], // 需要除草
needBug: [], // 需要除虫
canPutWeed: [], // 可以放草
canPutBug: [], // 可以放虫
};
for (const land of lands) {
const id = toNum(land.id);
const plant = land.plant;
// 是否显示此好友的调试信息
const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === friendName;
if (!plant || !plant.phases || plant.phases.length === 0) {
if (showDebug) console.log(` [${friendName}] 土地#${id}: 无植物或无阶段数据`);
continue;
}
const currentPhase = getCurrentPhase(plant.phases, showDebug, `[${friendName}]土地#${id}`);
if (!currentPhase) {
if (showDebug) console.log(` [${friendName}] 土地#${id}: getCurrentPhase返回null`);
continue;
}
const phaseVal = currentPhase.phase;
if (showDebug) {
const insectOwners = plant.insect_owners || [];
const weedOwners = plant.weed_owners || [];
console.log(` [${friendName}] 土地#${id}: phase=${phaseVal} stealable=${plant.stealable} dry=${toNum(plant.dry_num)} weed=${weedOwners.length} bug=${insectOwners.length}`);
}
if (phaseVal === PlantPhase.MATURE) {
if (plant.stealable) {
result.stealable.push(id);
const plantId = toNum(plant.id);
const plantName = getPlantName(plantId) || plant.name || '未知';
result.stealableInfo.push({ landId: id, plantId, name: plantName });
} else if (showDebug) {
console.log(` [${friendName}] 土地#${id}: 成熟但stealable=false (可能已被偷过)`);
}
continue;
}
if (phaseVal === PlantPhase.DEAD) continue;
// 帮助操作
if (toNum(plant.dry_num) > 0) result.needWater.push(id);
if (plant.weed_owners && plant.weed_owners.length > 0) result.needWeed.push(id);
if (plant.insect_owners && plant.insect_owners.length > 0) result.needBug.push(id);
// 捣乱操作: 检查是否可以放草/放虫
// 条件: 没有草且我没放过草
const weedOwners = plant.weed_owners || [];
const insectOwners = plant.insect_owners || [];
const iAlreadyPutWeed = weedOwners.some(gid => toNum(gid) === myGid);
const iAlreadyPutBug = insectOwners.some(gid => toNum(gid) === myGid);
// 每块地最多2个草/虫,且我没放过
if (weedOwners.length < 2 && !iAlreadyPutWeed) {
result.canPutWeed.push(id);
}
if (insectOwners.length < 2 && !iAlreadyPutBug) {
result.canPutBug.push(id);
}
}
return result;
}
// ============ 拜访好友 ============
async function visitFriend(friend, totalActions, myGid) {
const { gid, name } = friend;
const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === name;
if (showDebug) {
console.log(`\n========== 调试: 进入好友 [${name}] 农场 ==========`);
}
let enterReply;
try {
enterReply = await enterFriendFarm(gid);
} catch (e) {
logWarn('好友', `进入 ${name} 农场失败: ${e.message}`);
return;
}
const lands = enterReply.lands || [];
if (showDebug) {
console.log(` [${name}] 获取到 ${lands.length} 块土地`);
}
if (lands.length === 0) {
await leaveFriendFarm(gid);
return;
}
const status = analyzeFriendLands(lands, myGid, name);
if (showDebug) {
console.log(` [${name}] 分析结果: 可偷=${status.stealable.length} 浇水=${status.needWater.length} 除草=${status.needWeed.length} 除虫=${status.needBug.length}`);
console.log(`========== 调试结束 ==========\n`);
}
// 执行操作
const actions = [];
// 帮助操作: 只在有经验时执行 (如果启用了 HELP_ONLY_WITH_EXP)
if (status.needWeed.length > 0) {
const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10005); // 10005=除草
if (shouldHelp) {
let ok = 0;
for (const landId of status.needWeed) {
try { await helpWeed(gid, [landId]); ok++; } catch (e) { /* ignore */ }
await sleep(100);
}
if (ok > 0) { actions.push(`${ok}`); totalActions.weed += ok; }
}
}
if (status.needBug.length > 0) {
const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10006); // 10006=除虫
if (shouldHelp) {
let ok = 0;
for (const landId of status.needBug) {
try { await helpInsecticide(gid, [landId]); ok++; } catch (e) { /* ignore */ }
await sleep(100);
}
if (ok > 0) { actions.push(`${ok}`); totalActions.bug += ok; }
}
}
if (status.needWater.length > 0) {
const shouldHelp = !HELP_ONLY_WITH_EXP || canGetExp(10007); // 10007=浇水
if (shouldHelp) {
let ok = 0;
for (const landId of status.needWater) {
try { await helpWater(gid, [landId]); ok++; } catch (e) { /* ignore */ }
await sleep(100);
}
if (ok > 0) { actions.push(`${ok}`); totalActions.water += ok; }
}
}
// 偷菜: 始终执行
if (status.stealable.length > 0) {
let ok = 0;
const stolenPlants = [];
for (let i = 0; i < status.stealable.length; i++) {
const landId = status.stealable[i];
try {
await stealHarvest(gid, [landId]);
ok++;
if (status.stealableInfo[i]) {
stolenPlants.push(status.stealableInfo[i].name);
}
} catch (e) { /* ignore */ }
await sleep(100);
}
if (ok > 0) {
const plantNames = [...new Set(stolenPlants)].join('/');
actions.push(`${ok}${plantNames ? '(' + plantNames + ')' : ''}`);
totalActions.steal += ok;
}
}
// 捣乱操作: 放虫(10004)/放草(10003)
if (ENABLE_PUT_BAD_THINGS && status.canPutBug.length > 0 && canOperate(10004)) {
let ok = 0;
const remaining = getRemainingTimes(10004);
const toProcess = status.canPutBug.slice(0, remaining);
for (const landId of toProcess) {
if (!canOperate(10004)) break;
try { await putInsects(gid, [landId]); ok++; } catch (e) { /* ignore */ }
await sleep(100);
}
if (ok > 0) { actions.push(`放虫${ok}`); totalActions.putBug += ok; }
}
if (ENABLE_PUT_BAD_THINGS && status.canPutWeed.length > 0 && canOperate(10003)) {
let ok = 0;
const remaining = getRemainingTimes(10003);
const toProcess = status.canPutWeed.slice(0, remaining);
for (const landId of toProcess) {
if (!canOperate(10003)) break;
try { await putWeeds(gid, [landId]); ok++; } catch (e) { /* ignore */ }
await sleep(100);
}
if (ok > 0) { actions.push(`放草${ok}`); totalActions.putWeed += ok; }
}
if (actions.length > 0) {
log('好友', `${name}: ${actions.join('/')}`);
}
await leaveFriendFarm(gid);
}
// ============ 好友巡查主循环 ============
async function checkFriends() {
const state = getUserState();
if (isCheckingFriends || !state.gid) return;
isCheckingFriends = true;
// 检查是否跨日需要重置
checkDailyReset();
// 经验限制状态(移到有操作时才显示)
try {
const friendsReply = await getAllFriends();
const friends = friendsReply.game_friends || [];
if (friends.length === 0) { log('好友', '没有好友'); return; }
// 检查是否还有捣乱次数 (放虫/放草)
const canPutBugOrWeed = canOperate(10004) || canOperate(10003); // 10004=放虫, 10003=放草
// 分两类:有预览信息的优先访问,其他的放后面(用于放虫放草)
const priorityFriends = []; // 有可偷/可帮助的好友
const otherFriends = []; // 其他好友(仅用于放虫放草)
const visitedGids = new Set();
for (const f of friends) {
const gid = toNum(f.gid);
if (gid === state.gid) continue;
if (visitedGids.has(gid)) continue;
const name = f.remark || f.name || `GID:${gid}`;
const p = f.plant;
const stealNum = p ? toNum(p.steal_plant_num) : 0;
const dryNum = p ? toNum(p.dry_num) : 0;
const weedNum = p ? toNum(p.weed_num) : 0;
const insectNum = p ? toNum(p.insect_num) : 0;
// 调试:显示指定好友的预览信息
const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === name;
if (showDebug) {
console.log(`[调试] 好友列表预览 [${name}]: steal=${stealNum} dry=${dryNum} weed=${weedNum} insect=${insectNum}`);
}
// 只加入有预览信息的好友
if (stealNum > 0 || dryNum > 0 || weedNum > 0 || insectNum > 0) {
priorityFriends.push({ gid, name });
visitedGids.add(gid);
if (showDebug) {
console.log(`[调试] 好友 [${name}] 加入优先列表 (位置: ${priorityFriends.length})`);
}
} else if (ENABLE_PUT_BAD_THINGS && canPutBugOrWeed) {
// 没有预览信息但可以放虫放草(仅在开启放虫放草功能时)
otherFriends.push({ gid, name });
visitedGids.add(gid);
}
}
// 合并列表:优先好友在前
const friendsToVisit = [...priorityFriends, ...otherFriends];
// 调试:检查目标好友位置
if (DEBUG_FRIEND_LANDS && typeof DEBUG_FRIEND_LANDS === 'string') {
const idx = friendsToVisit.findIndex(f => f.name === DEBUG_FRIEND_LANDS);
if (idx >= 0) {
const inPriority = idx < priorityFriends.length;
console.log(`[调试] 好友 [${DEBUG_FRIEND_LANDS}] 位置: ${idx + 1}/${friendsToVisit.length} (${inPriority ? '优先列表' : '其他列表'})`);
} else {
console.log(`[调试] 好友 [${DEBUG_FRIEND_LANDS}] 不在待访问列表中!`);
}
}
if (friendsToVisit.length === 0) {
// 无需操作时不输出日志
return;
}
let totalActions = { steal: 0, water: 0, weed: 0, bug: 0, putBug: 0, putWeed: 0 };
for (let i = 0; i < friendsToVisit.length; i++) {
const friend = friendsToVisit[i];
const showDebug = DEBUG_FRIEND_LANDS === true || DEBUG_FRIEND_LANDS === friend.name;
if (showDebug) {
console.log(`[调试] 准备访问 [${friend.name}] (${i + 1}/${friendsToVisit.length})`);
}
try {
await visitFriend(friend, totalActions, state.gid);
} catch (e) {
if (showDebug) {
console.log(`[调试] 访问 [${friend.name}] 出错: ${e.message}`);
}
}
await sleep(500);
// 如果捣乱次数用完了,且没有其他操作,可以提前结束
if (!canOperate(10004) && !canOperate(10003)) { // 10004=放虫, 10003=放草
// 继续巡查,但不再放虫放草
}
}
// 只在有操作时输出日志
const summary = [];
if (totalActions.steal > 0) summary.push(`${totalActions.steal}`);
if (totalActions.weed > 0) summary.push(`除草${totalActions.weed}`);
if (totalActions.bug > 0) summary.push(`除虫${totalActions.bug}`);
if (totalActions.water > 0) summary.push(`浇水${totalActions.water}`);
if (totalActions.putBug > 0) summary.push(`放虫${totalActions.putBug}`);
if (totalActions.putWeed > 0) summary.push(`放草${totalActions.putWeed}`);
if (summary.length > 0) {
log('好友', `巡查 ${friendsToVisit.length} 人 → ${summary.join('/')}`);
}
isFirstFriendCheck = false;
} catch (err) {
logWarn('好友', `巡查失败: ${err.message}`);
} finally {
isCheckingFriends = false;
}
}
/**
* 好友巡查循环 - 本次完成后等待指定秒数再开始下次
*/
async function friendCheckLoop() {
while (friendLoopRunning) {
await checkFriends();
if (!friendLoopRunning) break;
await sleep(CONFIG.friendCheckInterval);
}
}
function startFriendCheckLoop() {
if (friendLoopRunning) return;
friendLoopRunning = true;
// 注册操作限制更新回调,从农场检查中获取限制信息
setOperationLimitsCallback(updateOperationLimits);
// 监听好友申请推送 (微信同玩)
networkEvents.on('friendApplicationReceived', onFriendApplicationReceived);
// 延迟 5 秒后启动循环,等待登录和首次农场检查完成
friendCheckTimer = setTimeout(() => friendCheckLoop(), 5000);
// 启动时检查一次待处理的好友申请
setTimeout(() => checkAndAcceptApplications(), 3000);
}
function stopFriendCheckLoop() {
friendLoopRunning = false;
networkEvents.off('friendApplicationReceived', onFriendApplicationReceived);
if (friendCheckTimer) { clearTimeout(friendCheckTimer); friendCheckTimer = null; }
}
// ============ 自动同意好友申请 (微信同玩) ============
/**
* 处理服务器推送的好友申请
*/
function onFriendApplicationReceived(applications) {
const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', ');
log('申请', `收到 ${applications.length} 个好友申请: ${names}`);
// 自动同意
const gids = applications.map(a => toNum(a.gid));
acceptFriendsWithRetry(gids);
}
/**
* 检查并同意所有待处理的好友申请
*/
async function checkAndAcceptApplications() {
try {
const reply = await getApplications();
const applications = reply.applications || [];
if (applications.length === 0) return;
const names = applications.map(a => a.name || `GID:${toNum(a.gid)}`).join(', ');
log('申请', `发现 ${applications.length} 个待处理申请: ${names}`);
const gids = applications.map(a => toNum(a.gid));
await acceptFriendsWithRetry(gids);
} catch (e) {
// 静默失败,可能是 QQ 平台不支持
}
}
/**
* 同意好友申请 (带重试)
*/
async function acceptFriendsWithRetry(gids) {
if (gids.length === 0) return;
try {
const reply = await acceptFriends(gids);
const friends = reply.friends || [];
if (friends.length > 0) {
const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', ');
log('申请', `已同意 ${friends.length} 人: ${names}`);
}
} catch (e) {
logWarn('申请', `同意失败: ${e.message}`);
}
}
module.exports = {
checkFriends, startFriendCheckLoop, stopFriendCheckLoop,
checkAndAcceptApplications,
};

View File

@@ -0,0 +1,269 @@
/**
* 游戏配置数据模块
* 从 gameConfig 目录加载配置数据
*/
const fs = require('fs');
const path = require('path');
// ============ 等级经验表 ============
let roleLevelConfig = null;
let levelExpTable = null; // 累计经验表,索引为等级
// ============ 植物配置 ============
let plantConfig = null;
let plantMap = new Map(); // id -> plant
let seedToPlant = new Map(); // seed_id -> plant
let fruitToPlant = new Map(); // fruit_id -> plant (果实ID -> 植物)
let nameToPlant = new Map();
/**
* 加载配置文件
*/
function loadConfigs() {
const configDir = path.join(__dirname, '..', 'gameConfig');
// 加载等级经验配置
try {
const roleLevelPath = path.join(configDir, 'RoleLevel.json');
if (fs.existsSync(roleLevelPath)) {
roleLevelConfig = JSON.parse(fs.readFileSync(roleLevelPath, 'utf8'));
// 构建累计经验表
levelExpTable = [];
for (const item of roleLevelConfig) {
levelExpTable[item.level] = item.exp;
}
console.log(`[配置] 已加载等级经验表 (${roleLevelConfig.length} 级)`);
}
} catch (e) {
console.warn('[配置] 加载 RoleLevel.json 失败:', e.message);
}
// 加载植物配置
try {
const plantPath = path.join(configDir, 'Plant.json');
if (fs.existsSync(plantPath)) {
plantConfig = JSON.parse(fs.readFileSync(plantPath, 'utf8'));
plantMap.clear();
seedToPlant.clear();
fruitToPlant.clear();
nameToPlant.clear();
for (const plant of plantConfig) {
plantMap.set(plant.id, plant);
if (plant.seed_id) {
seedToPlant.set(plant.seed_id, plant);
}
if (plant.fruit && plant.fruit.id) {
fruitToPlant.set(plant.fruit.id, plant);
}
if (plant.name) {
nameToPlant.set(plant.name, plant);
}
}
console.log(`[配置] 已加载植物配置 (${plantConfig.length} 种)`);
}
} catch (e) {
console.warn('[配置] 加载 Plant.json 失败:', e.message);
}
}
// ============ 等级经验相关 ============
/**
* 获取等级经验表
*/
function getLevelExpTable() {
return levelExpTable;
}
/**
* 计算当前等级的经验进度
* @param {number} level - 当前等级
* @param {number} totalExp - 累计总经验
* @returns {{ current: number, needed: number }} 当前等级经验进度
*/
function getLevelExpProgress(level, totalExp) {
if (!levelExpTable || level <= 0) return { current: 0, needed: 0 };
const currentLevelStart = levelExpTable[level] || 0;
const nextLevelStart = levelExpTable[level + 1] || (currentLevelStart + 100000);
const currentExp = Math.max(0, totalExp - currentLevelStart);
const neededExp = nextLevelStart - currentLevelStart;
return { current: currentExp, needed: neededExp };
}
// ============ 植物配置相关 ============
/**
* 根据植物ID获取植物信息
* @param {number} plantId - 植物ID
*/
function getPlantById(plantId) {
return plantMap.get(plantId);
}
function getPlantByName(name) {
return nameToPlant.get(name);
}
/**
* 根据种子ID获取植物信息
* @param {number} seedId - 种子ID
*/
function getPlantBySeedId(seedId) {
return seedToPlant.get(seedId);
}
function getSeedIdByPlantName(name) {
const plant = nameToPlant.get(name);
if (!plant) return 0;
return plant.seed_id || 0;
}
/**
* 获取植物名称
* @param {number} plantId - 植物ID
*/
function getPlantName(plantId) {
const plant = plantMap.get(plantId);
return plant ? plant.name : `植物${plantId}`;
}
/**
* 根据种子ID获取植物名称
* @param {number} seedId - 种子ID
*/
function getPlantNameBySeedId(seedId) {
const plant = seedToPlant.get(seedId);
return plant ? plant.name : `种子${seedId}`;
}
/**
* 获取植物的果实信息
* @param {number} plantId - 植物ID
* @returns {{ id: number, count: number, name: string } | null}
*/
function getPlantFruit(plantId) {
const plant = plantMap.get(plantId);
if (!plant || !plant.fruit) return null;
return {
id: plant.fruit.id,
count: plant.fruit.count,
name: plant.name,
};
}
/**
* 获取植物的生长时间(秒)
* @param {number} plantId - 植物ID
*/
function getPlantGrowTime(plantId) {
const plant = plantMap.get(plantId);
if (!plant || !plant.grow_phases) return 0;
// 解析 "种子:30;发芽:30;成熟:0;" 格式
const phases = plant.grow_phases.split(';').filter(p => p);
let totalSeconds = 0;
for (const phase of phases) {
const match = phase.match(/:(\d+)/);
if (match) {
totalSeconds += parseInt(match[1]);
}
}
return totalSeconds;
}
/**
* 格式化时间
* @param {number} seconds - 秒数
*/
function formatGrowTime(seconds) {
if (seconds < 60) return `${seconds}`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`;
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return mins > 0 ? `${hours}小时${mins}` : `${hours}小时`;
}
/**
* 获取植物的收获经验
* @param {number} plantId - 植物ID
*/
function getPlantExp(plantId) {
const plant = plantMap.get(plantId);
return plant ? plant.exp : 0;
}
/**
* 根据果实ID获取植物名称
* @param {number} fruitId - 果实ID
*/
function getFruitName(fruitId) {
const plant = fruitToPlant.get(fruitId);
return plant ? plant.name : `果实${fruitId}`;
}
/**
* 根据果实ID获取植物信息
* @param {number} fruitId - 果实ID
*/
function getPlantByFruitId(fruitId) {
return fruitToPlant.get(fruitId);
}
const ITEM_NAME_MAP = {
1001: '金币',
1002: '点券',
1011: '普通化肥容器',
1012: '有机化肥容器',
1013: '友谊果实',
1014: '穗华',
1015: '幸运币',
3001: '普通收藏点',
1101: '种植经验',
80001: '普通(1小时)',
80011: '有机(1小时)',
80002: '普通(4小时)',
80003: '普通(8小时)',
80004: '普通(12小时)',
80012: '有机(4小时)',
80013: '有机(8小时)',
80014: '有机(12小时)',
90004: '1天狗粮',
90005: '3天狗粮',
100003: '化肥礼包',
};
function getItemNameById(id) {
const name = ITEM_NAME_MAP[id];
if (name) return name;
return `物品${id}`;
}
// 启动时加载配置
loadConfigs();
module.exports = {
loadConfigs,
// 等级经验
getLevelExpTable,
getLevelExpProgress,
// 植物配置
getPlantById,
getPlantByName,
getPlantBySeedId,
getSeedIdByPlantName,
getPlantName,
getPlantNameBySeedId,
getPlantFruit,
getPlantGrowTime,
getPlantExp,
formatGrowTime,
// 果实配置
getFruitName,
getPlantByFruitId,
// 物品配置
getItemNameById,
};

161
211/server/src/invite.js Normal file
View File

@@ -0,0 +1,161 @@
/**
* 邀请码处理模块 - 读取 share.txt 并通过 ReportArkClick 申请好友
* 注意:此功能仅在微信环境下有效
*
* 原理:
* 1. 首次登录时,游戏会在 LoginRequest 中携带 sharer_id 和 sharer_open_id
* 2. 已登录状态下点击分享链接,游戏会发送 ReportArkClickRequest
* 3. 服务器收到后会自动向分享者发送好友申请
*
* 我们使用 ReportArkClickRequest 来模拟已登录状态下的分享链接点击
*/
const fs = require('fs');
const path = require('path');
const { types } = require('./proto');
const { sendMsgAsync } = require('./network');
const { toLong, log, logWarn, sleep } = require('./utils');
const { CONFIG } = require('./config');
/**
* 解析分享链接,提取 uid 和 openid
* 格式: ?uid=xxx&openid=xxx&share_source=xxx&doc_id=xxx
*/
function parseShareLink(link) {
const result = { uid: null, openid: null, shareSource: null, docId: null };
// 移除开头的 ? 如果有
const queryStr = link.startsWith('?') ? link.slice(1) : link;
// 解析参数
const params = new URLSearchParams(queryStr);
result.uid = params.get('uid');
result.openid = params.get('openid');
result.shareSource = params.get('share_source');
result.docId = params.get('doc_id');
return result;
}
/**
* 读取 share.txt 文件并去重
*/
function readShareFile() {
const shareFilePath = path.join(__dirname, '..', 'share.txt');
if (!fs.existsSync(shareFilePath)) {
return [];
}
try {
const content = fs.readFileSync(shareFilePath, 'utf8');
const lines = content.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && line.includes('openid='));
const invites = [];
const seenUids = new Set(); // 用于去重
for (const line of lines) {
const parsed = parseShareLink(line);
if (parsed.openid && parsed.uid) {
// 按 uid 去重,同一个用户只处理一次
if (!seenUids.has(parsed.uid)) {
seenUids.add(parsed.uid);
invites.push(parsed);
}
}
}
return invites;
} catch (e) {
logWarn('邀请', `读取 share.txt 失败: ${e.message}`);
return [];
}
}
/**
* 发送 ReportArkClick 请求
* 模拟已登录状态下点击分享链接,触发服务器向分享者发送好友申请
*/
async function sendReportArkClick(sharerId, sharerOpenId, shareSource) {
const body = types.ReportArkClickRequest.encode(types.ReportArkClickRequest.create({
sharer_id: toLong(sharerId),
sharer_open_id: sharerOpenId,
share_cfg_id: toLong(shareSource || 0),
scene_id: '1256', // 模拟微信场景
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.userpb.UserService', 'ReportArkClick', body);
return types.ReportArkClickReply.decode(replyBody);
}
// 请求间隔时间(毫秒)
const INVITE_REQUEST_DELAY = 2000;
/**
* 处理邀请码列表
* 仅在微信环境下执行
*/
async function processInviteCodes() {
// 检查是否为微信环境
if (CONFIG.platform !== 'wx') {
log('邀请', '当前为 QQ 环境,跳过邀请码处理(仅微信支持)');
return;
}
const invites = readShareFile();
if (invites.length === 0) {
return;
}
log('邀请', `读取到 ${invites.length} 个邀请码(已去重),开始逐个处理...`);
let successCount = 0;
let failCount = 0;
for (let i = 0; i < invites.length; i++) {
const invite = invites[i];
try {
// 发送 ReportArkClick 请求,模拟点击分享链接
await sendReportArkClick(invite.uid, invite.openid, invite.shareSource);
successCount++;
log('邀请', `[${i + 1}/${invites.length}] 已向 uid=${invite.uid} 发送好友申请`);
} catch (e) {
failCount++;
logWarn('邀请', `[${i + 1}/${invites.length}] 向 uid=${invite.uid} 发送申请失败: ${e.message}`);
}
// 每个请求之间延迟,避免请求过快被限流
if (i < invites.length - 1) {
await sleep(INVITE_REQUEST_DELAY);
}
}
log('邀请', `处理完成: 成功 ${successCount}, 失败 ${failCount}`);
// 处理完成后清空文件
clearShareFile();
}
/**
* 清空已处理的邀请码文件
*/
function clearShareFile() {
const shareFilePath = path.join(__dirname, '..', 'share.txt');
try {
fs.writeFileSync(shareFilePath, '', 'utf8');
log('邀请', '已清空 share.txt');
} catch (e) {
// 静默失败
}
}
module.exports = {
parseShareLink,
readShareFile,
sendReportArkClick,
processInviteCodes,
clearShareFile,
};

461
211/server/src/network.js Normal file
View File

@@ -0,0 +1,461 @@
/**
* WebSocket 网络层 - 连接/消息编解码/登录/心跳
*/
const WebSocket = require('ws');
const EventEmitter = require('events');
const { CONFIG } = require('./config');
const { types } = require('./proto');
const { toLong, toNum, syncServerTime, log, logWarn } = require('./utils');
const { updateStatusFromLogin, updateStatusGold, updateStatusLevel } = require('./status');
// ============ 事件发射器 (用于推送通知) ============
const networkEvents = new EventEmitter();
// ============ 内部状态 ============
let ws = null;
let clientSeq = 1;
let serverSeq = 0;
let heartbeatTimer = null;
let pendingCallbacks = new Map();
// ============ 用户状态 (登录后设置) ============
const userState = {
gid: 0,
name: '',
level: 0,
gold: 0,
exp: 0,
};
function getUserState() { return userState; }
// ============ 消息编解码 ============
function encodeMsg(serviceName, methodName, bodyBytes) {
const msg = types.GateMessage.create({
meta: {
service_name: serviceName,
method_name: methodName,
message_type: 1,
client_seq: toLong(clientSeq),
server_seq: toLong(serverSeq),
},
body: bodyBytes || Buffer.alloc(0),
});
const encoded = types.GateMessage.encode(msg).finish();
clientSeq++;
return encoded;
}
function sendMsg(serviceName, methodName, bodyBytes, callback) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
log('WS', '连接未打开');
return false;
}
const seq = clientSeq;
const encoded = encodeMsg(serviceName, methodName, bodyBytes);
if (callback) pendingCallbacks.set(seq, callback);
ws.send(encoded);
return true;
}
/** Promise 版发送 */
function sendMsgAsync(serviceName, methodName, bodyBytes, timeout = 10000) {
return new Promise((resolve, reject) => {
// 检查连接状态
if (!ws || ws.readyState !== WebSocket.OPEN) {
reject(new Error(`连接未打开: ${methodName}`));
return;
}
const seq = clientSeq;
const timer = setTimeout(() => {
pendingCallbacks.delete(seq);
// 检查当前待处理的请求数
const pending = pendingCallbacks.size;
reject(new Error(`请求超时: ${methodName} (seq=${seq}, pending=${pending})`));
}, timeout);
const sent = sendMsg(serviceName, methodName, bodyBytes, (err, body, meta) => {
clearTimeout(timer);
if (err) reject(err);
else resolve({ body, meta });
});
if (!sent) {
clearTimeout(timer);
reject(new Error(`发送失败: ${methodName}`));
}
});
}
// ============ 消息处理 ============
function handleMessage(data) {
try {
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
const msg = types.GateMessage.decode(buf);
const meta = msg.meta;
if (!meta) return;
if (meta.server_seq) {
const seq = toNum(meta.server_seq);
if (seq > serverSeq) serverSeq = seq;
}
const msgType = meta.message_type;
// Notify
if (msgType === 3) {
handleNotify(msg);
return;
}
// Response
if (msgType === 2) {
const errorCode = toNum(meta.error_code);
const clientSeqVal = toNum(meta.client_seq);
const cb = pendingCallbacks.get(clientSeqVal);
if (cb) {
pendingCallbacks.delete(clientSeqVal);
if (errorCode !== 0) {
cb(new Error(`${meta.service_name}.${meta.method_name} 错误: code=${errorCode} ${meta.error_message || ''}`));
} else {
cb(null, msg.body, meta);
}
return;
}
if (errorCode !== 0) {
logWarn('错误', `${meta.service_name}.${meta.method_name} code=${errorCode} ${meta.error_message || ''}`);
}
}
} catch (err) {
logWarn('解码', err.message);
}
}
// 调试:记录所有推送类型 (设为 true 可查看所有推送)
// 注意QQ环境下只有 ItemNotify 推送,没有 LandsNotify 推送
const DEBUG_NOTIFY = false;
function handleNotify(msg) {
if (!msg.body || msg.body.length === 0) return;
try {
const event = types.EventMessage.decode(msg.body);
const type = event.message_type || '';
const eventBody = event.body;
// 调试:显示所有推送类型
if (DEBUG_NOTIFY) {
console.log(`[DEBUG] 收到推送: ${type}`);
}
// 被踢下线
if (type.includes('Kickout')) {
log('推送', `被踢下线! ${type}`);
try {
const notify = types.KickoutNotify.decode(eventBody);
log('推送', `原因: ${notify.reason_message || '未知'}`);
} catch (e) { }
return;
}
// 土地状态变化 (被放虫/放草/偷菜等)
if (type.includes('LandsNotify')) {
try {
const notify = types.LandsNotify.decode(eventBody);
const hostGid = toNum(notify.host_gid);
const lands = notify.lands || [];
if (DEBUG_NOTIFY) {
console.log(`[DEBUG] LandsNotify: hostGid=${hostGid}, myGid=${userState.gid}, lands=${lands.length}`);
}
if (lands.length > 0) {
// 如果是自己的农场,触发事件
if (hostGid === userState.gid || hostGid === 0) {
networkEvents.emit('landsChanged', lands);
}
}
} catch (e) { }
return;
}
// 物品变化通知 (经验/金币等) - 仅更新状态栏
// 金币: id=1 或 id=1001 (GodItemId)
// 经验: id=1101 (ExpItemId) 或 id=2
if (type.includes('ItemNotify')) {
try {
const notify = types.ItemNotify.decode(eventBody);
const items = notify.items || [];
for (const itemChg of items) {
const item = itemChg.item;
if (!item) continue;
const id = toNum(item.id);
const count = toNum(item.count);
if (id === 1101 || id === 2) {
userState.exp = count;
updateStatusLevel(userState.level, count);
} else if (id === 1 || id === 1001) {
userState.gold = count;
updateStatusGold(count);
}
}
} catch (e) { }
return;
}
// 基本信息变化 (升级等)
if (type.includes('BasicNotify')) {
try {
const notify = types.BasicNotify.decode(eventBody);
if (notify.basic) {
const oldLevel = userState.level;
const oldExp = userState.exp || 0;
userState.level = toNum(notify.basic.level) || userState.level;
userState.gold = toNum(notify.basic.gold) || userState.gold;
const exp = toNum(notify.basic.exp);
if (exp > 0) {
userState.exp = exp;
updateStatusLevel(userState.level, exp);
}
updateStatusGold(userState.gold);
// 升级提示
if (userState.level !== oldLevel) {
log('系统', `升级! Lv${oldLevel} → Lv${userState.level}`);
}
}
} catch (e) { }
return;
}
// 好友申请通知 (微信同玩)
if (type.includes('FriendApplicationReceivedNotify')) {
try {
const notify = types.FriendApplicationReceivedNotify.decode(eventBody);
const applications = notify.applications || [];
if (applications.length > 0) {
networkEvents.emit('friendApplicationReceived', applications);
}
} catch (e) { }
return;
}
// 好友添加成功通知
if (type.includes('FriendAddedNotify')) {
try {
const notify = types.FriendAddedNotify.decode(eventBody);
const friends = notify.friends || [];
if (friends.length > 0) {
const names = friends.map(f => f.name || f.remark || `GID:${toNum(f.gid)}`).join(', ');
log('好友', `新好友: ${names}`);
}
} catch (e) { }
return;
}
// 物品变化通知 (收获/购买/消耗等)
if (type.includes('ItemNotify')) {
try {
const notify = types.ItemNotify.decode(eventBody);
const items = notify.items || [];
for (const chg of items) {
if (!chg.item) continue;
const id = toNum(chg.item.id);
const count = toNum(chg.item.count);
const delta = toNum(chg.delta);
// 金币 ID=1
if (id === 1) {
userState.gold = count;
updateStatusGold(count);
if (delta !== 0) {
log('物品', `金币 ${delta > 0 ? '+' : ''}${delta} (当前: ${count})`);
}
}
// 经验 ID=2 (升级由 BasicNotify 处理)
}
} catch (e) { }
return;
}
// 商品解锁通知 (升级后解锁新种子等)
if (type.includes('GoodsUnlockNotify')) {
try {
const notify = types.GoodsUnlockNotify.decode(eventBody);
const goods = notify.goods_list || [];
if (goods.length > 0) {
log('商店', `解锁 ${goods.length} 个新商品!`);
}
} catch (e) { }
return;
}
// 任务状态变化通知
if (type.includes('TaskInfoNotify')) {
try {
const notify = types.TaskInfoNotify.decode(eventBody);
if (notify.task_info) {
networkEvents.emit('taskInfoNotify', notify.task_info);
}
} catch (e) { }
return;
}
// 其他未处理的推送类型 (调试用)
// log('推送', `未处理类型: ${type}`);
} catch (e) {
logWarn('推送', `解码失败: ${e.message}`);
}
}
// ============ 登录 ============
function sendLogin(onLoginSuccess) {
const body = types.LoginRequest.encode(types.LoginRequest.create({
sharer_id: toLong(0),
sharer_open_id: '',
device_info: {
client_version: CONFIG.clientVersion,
sys_software: 'iOS 26.2.1',
network: 'wifi',
memory: '7672',
device_id: 'iPhone X<iPhone18,3>',
},
share_cfg_id: toLong(0),
scene_id: '1256',
report_data: {
callback: '', cd_extend_info: '', click_id: '', clue_token: '',
minigame_channel: 'other', minigame_platid: 2, req_id: '', trackid: '',
},
})).finish();
sendMsg('gamepb.userpb.UserService', 'Login', body, (err, bodyBytes, meta) => {
if (err) {
log('登录', `失败: ${err.message}`);
return;
}
try {
const reply = types.LoginReply.decode(bodyBytes);
if (reply.basic) {
userState.gid = toNum(reply.basic.gid);
userState.name = reply.basic.name || '未知';
userState.level = toNum(reply.basic.level);
userState.gold = toNum(reply.basic.gold);
userState.exp = toNum(reply.basic.exp);
// 更新状态栏
updateStatusFromLogin({
name: userState.name,
level: userState.level,
gold: userState.gold,
exp: userState.exp,
});
console.log('');
console.log('========== 登录成功 ==========');
console.log(` GID: ${userState.gid}`);
console.log(` 昵称: ${userState.name}`);
console.log(` 等级: ${userState.level}`);
console.log(` 金币: ${userState.gold}`);
if (reply.time_now_millis) {
syncServerTime(toNum(reply.time_now_millis));
console.log(` 时间: ${new Date(toNum(reply.time_now_millis)).toLocaleString()}`);
}
console.log('===============================');
console.log('');
}
startHeartbeat();
if (onLoginSuccess) onLoginSuccess();
} catch (e) {
log('登录', `解码失败: ${e.message}`);
}
});
}
// ============ 心跳 ============
let lastHeartbeatResponse = Date.now();
let heartbeatMissCount = 0;
function startHeartbeat() {
if (heartbeatTimer) clearInterval(heartbeatTimer);
lastHeartbeatResponse = Date.now();
heartbeatMissCount = 0;
heartbeatTimer = setInterval(() => {
if (!userState.gid) return;
// 检查上次心跳响应时间,超过 60 秒没响应说明连接有问题
const timeSinceLastResponse = Date.now() - lastHeartbeatResponse;
if (timeSinceLastResponse > 60000) {
heartbeatMissCount++;
logWarn('心跳', `连接可能已断开 (${Math.round(timeSinceLastResponse/1000)}s 无响应, pending=${pendingCallbacks.size})`);
if (heartbeatMissCount >= 2) {
log('心跳', '尝试重连...');
// 清理待处理的回调,避免堆积
pendingCallbacks.forEach((cb, seq) => {
try { cb(new Error('连接超时,已清理')); } catch (e) {}
});
pendingCallbacks.clear();
}
}
const body = types.HeartbeatRequest.encode(types.HeartbeatRequest.create({
gid: toLong(userState.gid),
client_version: CONFIG.clientVersion,
})).finish();
sendMsg('gamepb.userpb.UserService', 'Heartbeat', body, (err, replyBody) => {
if (err || !replyBody) return;
lastHeartbeatResponse = Date.now();
heartbeatMissCount = 0;
try {
const reply = types.HeartbeatReply.decode(replyBody);
if (reply.server_time) syncServerTime(toNum(reply.server_time));
} catch (e) { }
});
}, CONFIG.heartbeatInterval);
}
// ============ WebSocket 连接 ============
function connect(code, onLoginSuccess) {
const url = `${CONFIG.serverUrl}?platform=${CONFIG.platform}&os=${CONFIG.os}&ver=${CONFIG.clientVersion}&code=${code}&openID=`;
ws = new WebSocket(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13)',
'Origin': 'https://gate-obt.nqf.qq.com',
},
});
ws.binaryType = 'arraybuffer';
ws.on('open', () => {
sendLogin(onLoginSuccess);
});
ws.on('message', (data) => {
handleMessage(Buffer.isBuffer(data) ? data : Buffer.from(data));
});
ws.on('close', (code, reason) => {
console.log(`[WS] 连接关闭 (code=${code})`);
cleanup();
});
ws.on('error', (err) => {
logWarn('WS', `错误: ${err.message}`);
});
}
function cleanup() {
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
pendingCallbacks.clear();
}
function getWs() { return ws; }
module.exports = {
connect, cleanup, getWs,
sendMsg, sendMsgAsync,
getUserState,
networkEvents,
};

121
211/server/src/proto.js Normal file
View File

@@ -0,0 +1,121 @@
/**
* Proto 加载与消息类型管理
*/
const protobuf = require('protobufjs');
const path = require('path');
const { log } = require('./utils');
// Proto 根对象与所有消息类型
let root = null;
const types = {};
async function loadProto() {
const protoDir = path.join(__dirname, '..', 'proto');
root = new protobuf.Root();
await root.load([
path.join(protoDir, 'game.proto'),
path.join(protoDir, 'userpb.proto'),
path.join(protoDir, 'plantpb.proto'),
path.join(protoDir, 'corepb.proto'),
path.join(protoDir, 'shoppb.proto'),
path.join(protoDir, 'friendpb.proto'),
path.join(protoDir, 'visitpb.proto'),
path.join(protoDir, 'notifypb.proto'),
path.join(protoDir, 'taskpb.proto'),
path.join(protoDir, 'itempb.proto'),
], { keepCase: true });
// 网关
types.GateMessage = root.lookupType('gatepb.Message');
types.GateMeta = root.lookupType('gatepb.Meta');
types.EventMessage = root.lookupType('gatepb.EventMessage');
// 用户
types.LoginRequest = root.lookupType('gamepb.userpb.LoginRequest');
types.LoginReply = root.lookupType('gamepb.userpb.LoginReply');
types.HeartbeatRequest = root.lookupType('gamepb.userpb.HeartbeatRequest');
types.HeartbeatReply = root.lookupType('gamepb.userpb.HeartbeatReply');
types.ReportArkClickRequest = root.lookupType('gamepb.userpb.ReportArkClickRequest');
types.ReportArkClickReply = root.lookupType('gamepb.userpb.ReportArkClickReply');
// 农场
types.AllLandsRequest = root.lookupType('gamepb.plantpb.AllLandsRequest');
types.AllLandsReply = root.lookupType('gamepb.plantpb.AllLandsReply');
types.HarvestRequest = root.lookupType('gamepb.plantpb.HarvestRequest');
types.HarvestReply = root.lookupType('gamepb.plantpb.HarvestReply');
types.WaterLandRequest = root.lookupType('gamepb.plantpb.WaterLandRequest');
types.WaterLandReply = root.lookupType('gamepb.plantpb.WaterLandReply');
types.WeedOutRequest = root.lookupType('gamepb.plantpb.WeedOutRequest');
types.WeedOutReply = root.lookupType('gamepb.plantpb.WeedOutReply');
types.InsecticideRequest = root.lookupType('gamepb.plantpb.InsecticideRequest');
types.InsecticideReply = root.lookupType('gamepb.plantpb.InsecticideReply');
types.RemovePlantRequest = root.lookupType('gamepb.plantpb.RemovePlantRequest');
types.RemovePlantReply = root.lookupType('gamepb.plantpb.RemovePlantReply');
types.PutInsectsRequest = root.lookupType('gamepb.plantpb.PutInsectsRequest');
types.PutInsectsReply = root.lookupType('gamepb.plantpb.PutInsectsReply');
types.PutWeedsRequest = root.lookupType('gamepb.plantpb.PutWeedsRequest');
types.PutWeedsReply = root.lookupType('gamepb.plantpb.PutWeedsReply');
types.FertilizeRequest = root.lookupType('gamepb.plantpb.FertilizeRequest');
types.FertilizeReply = root.lookupType('gamepb.plantpb.FertilizeReply');
// 背包/仓库
types.BagRequest = root.lookupType('gamepb.itempb.BagRequest');
types.BagReply = root.lookupType('gamepb.itempb.BagReply');
types.SellRequest = root.lookupType('gamepb.itempb.SellRequest');
types.SellReply = root.lookupType('gamepb.itempb.SellReply');
types.UseRequest = root.lookupType('gamepb.itempb.UseRequest');
types.UseReply = root.lookupType('gamepb.itempb.UseReply');
types.BatchUseRequest = root.lookupType('gamepb.itempb.BatchUseRequest');
types.BatchUseReply = root.lookupType('gamepb.itempb.BatchUseReply');
types.PlantRequest = root.lookupType('gamepb.plantpb.PlantRequest');
types.PlantReply = root.lookupType('gamepb.plantpb.PlantReply');
// 商店
types.ShopProfilesRequest = root.lookupType('gamepb.shoppb.ShopProfilesRequest');
types.ShopProfilesReply = root.lookupType('gamepb.shoppb.ShopProfilesReply');
types.ShopInfoRequest = root.lookupType('gamepb.shoppb.ShopInfoRequest');
types.ShopInfoReply = root.lookupType('gamepb.shoppb.ShopInfoReply');
types.BuyGoodsRequest = root.lookupType('gamepb.shoppb.BuyGoodsRequest');
types.BuyGoodsReply = root.lookupType('gamepb.shoppb.BuyGoodsReply');
// 好友
types.GetAllFriendsRequest = root.lookupType('gamepb.friendpb.GetAllRequest');
types.GetAllFriendsReply = root.lookupType('gamepb.friendpb.GetAllReply');
types.GetApplicationsRequest = root.lookupType('gamepb.friendpb.GetApplicationsRequest');
types.GetApplicationsReply = root.lookupType('gamepb.friendpb.GetApplicationsReply');
types.AcceptFriendsRequest = root.lookupType('gamepb.friendpb.AcceptFriendsRequest');
types.AcceptFriendsReply = root.lookupType('gamepb.friendpb.AcceptFriendsReply');
// 访问
types.VisitEnterRequest = root.lookupType('gamepb.visitpb.EnterRequest');
types.VisitEnterReply = root.lookupType('gamepb.visitpb.EnterReply');
types.VisitLeaveRequest = root.lookupType('gamepb.visitpb.LeaveRequest');
types.VisitLeaveReply = root.lookupType('gamepb.visitpb.LeaveReply');
// 任务
types.TaskInfoRequest = root.lookupType('gamepb.taskpb.TaskInfoRequest');
types.TaskInfoReply = root.lookupType('gamepb.taskpb.TaskInfoReply');
types.ClaimTaskRewardRequest = root.lookupType('gamepb.taskpb.ClaimTaskRewardRequest');
types.ClaimTaskRewardReply = root.lookupType('gamepb.taskpb.ClaimTaskRewardReply');
types.BatchClaimTaskRewardRequest = root.lookupType('gamepb.taskpb.BatchClaimTaskRewardRequest');
types.BatchClaimTaskRewardReply = root.lookupType('gamepb.taskpb.BatchClaimTaskRewardReply');
// 服务器推送通知
types.LandsNotify = root.lookupType('gamepb.plantpb.LandsNotify');
types.BasicNotify = root.lookupType('gamepb.userpb.BasicNotify');
types.KickoutNotify = root.lookupType('gatepb.KickoutNotify');
types.FriendApplicationReceivedNotify = root.lookupType('gamepb.friendpb.FriendApplicationReceivedNotify');
types.FriendAddedNotify = root.lookupType('gamepb.friendpb.FriendAddedNotify');
types.ItemNotify = root.lookupType('gamepb.itempb.ItemNotify');
types.GoodsUnlockNotify = root.lookupType('gamepb.shoppb.GoodsUnlockNotify');
types.TaskInfoNotify = root.lookupType('gamepb.taskpb.TaskInfoNotify');
// Proto 加载完成
}
function getRoot() {
return root;
}
module.exports = { loadProto, types, getRoot };

View File

@@ -0,0 +1,314 @@
const axios = require('axios');
const { CookieUtils, HashUtils } = require('./utils');
// User Agent Definition
const ChromeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
class QRLoginSession {
/**
* Presets for different login targets
*/
static Presets = {
vip: {
name: 'QQ会员 (VIP)',
description: 'QQ会员官网',
aid: '8000201',
daid: '18',
redirectUri: 'https://vip.qq.com/loginsuccess.html',
referrer: 'https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=8000201&style=20&s_url=https%3A%2F%2Fvip.qq.com%2Floginsuccess.html&maskOpacity=60&daid=18&target=self',
},
qzone: {
name: 'QQ空间 (QZone)',
description: 'QQ空间网页版',
aid: '549000912',
daid: '5',
redirectUri: 'https://qzs.qzone.qq.com/qzone/v5/loginsucc.html?para=izone',
referrer: 'https://qzone.qq.com/',
},
music: {
name: 'QQ音乐 (Music)',
description: 'QQ音乐网页版',
aid: '716027609',
daid: '383',
redirectUri: 'https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https%3A%2F%2Fy.qq.com%2F',
ptThirdAid: '100497308',
responseType: 'code',
openapi: '1010_1030',
},
wegame: {
name: 'WeGame',
description: 'WeGame 平台',
aid: '1600001063',
daid: '733',
redirectUri: 'https://www.wegame.com.cn/middle/login/third_callback.html',
referrer: 'https://www.wegame.com.cn/',
},
val: {
name: '瓦罗兰特 (VAL)',
description: '无畏契约官网',
aid: '716027609',
daid: '383',
redirectUri: 'https://val.qq.com/comm-htdocs/login/qc_redirect.html?parent_domain=https%3A%2F%2Fval.qq.com&isMiloSDK=1&isPc=1',
ptThirdAid: '102059301',
responseType: 'code',
openapi: '1010_1030',
},
};
/**
* Request a new QR Code
* @param {string} presetKey - The key of the preset to use (vip, qzone, etc)
*/
static async requestQRCode(presetKey) {
const config = this.Presets[presetKey] || this.Presets.vip;
const params = new URLSearchParams({
appid: config.aid,
e: '2',
l: 'M',
s: '3',
d: '72',
v: '4',
t: String(Math.random()),
daid: config.daid,
});
if (config.ptThirdAid) {
params.set('pt_3rd_aid', config.ptThirdAid);
params.set('u1', 'https://graph.qq.com/oauth2.0/login_jump');
} else {
params.set('u1', config.redirectUri);
}
const url = `https://ssl.ptlogin2.qq.com/ptqrshow?${params.toString()}`;
try {
const response = await axios.get(url, {
responseType: 'arraybuffer',
headers: {
'Referer': config.referrer || `https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=${config.aid}&style=20&s_url=${encodeURIComponent(config.redirectUri)}&maskOpacity=60&daid=${config.daid}&target=self`,
'User-Agent': ChromeUA,
}
});
const setCookie = response.headers['set-cookie'];
const qrsig = CookieUtils.getValue(setCookie, 'qrsig');
const qrcodeBase64 = Buffer.from(response.data).toString('base64');
return { qrsig, qrcode: `data:image/png;base64,${qrcodeBase64}`, url };
} catch (error) {
console.error('Request QRCode Error:', error);
throw error;
}
}
/**
* Check the status of the QR Code
* @param {string} qrsig
* @param {string} presetKey
*/
static async checkStatus(qrsig, presetKey) {
const config = this.Presets[presetKey] || this.Presets.vip;
const ptqrtoken = HashUtils.hash(qrsig);
const params = new URLSearchParams({
ptqrtoken: String(ptqrtoken),
from_ui: '1',
aid: config.aid,
daid: config.daid,
action: `0-0-${Date.now()}`, // Added timestamp
pt_uistyle: '40',
js_ver: '21020514',
js_type: '1'
});
if (config.ptThirdAid) {
params.set('pt_3rd_aid', config.ptThirdAid);
params.set('u1', 'https://graph.qq.com/oauth2.0/login_jump');
} else {
params.set('u1', config.redirectUri);
}
const api = `https://ssl.ptlogin2.qq.com/ptqrlogin?${params.toString()}`;
try {
const response = await axios.get(api, {
headers: {
'Cookie': `qrsig=${qrsig}`,
'Referer': config.referrer || 'https://xui.ptlogin2.qq.com/',
'User-Agent': ChromeUA,
},
});
const text = response.data;
// Parse response: ptuiCB('66','0','','0','二维码未失效。(3776510309)', '')
// Robust parsing using Regex to handle commas in content
const matcher = /ptuiCB\((.+)\)/;
const match = text.match(matcher);
if (!match) {
throw new Error('Invalid response format');
}
// Extract arguments: 'arg1', 'arg2', ...
// This regex matches single-quoted strings: '([^']*)'
const args = [];
const argMatcher = /'([^']*)'/g;
let argMatch;
while ((argMatch = argMatcher.exec(match[1])) !== null) {
args.push(argMatch[1]);
}
const [ret, extret, jumpUrl, redirect, msg, nickname] = args;
return {
ret,
msg,
nickname,
jumpUrl,
cookie: response.headers['set-cookie'] // Return cookies to frontend if success
};
} catch (error) {
console.error('Check Status Error:', error);
throw new Error('Check status failed');
}
}
/**
* Get final cookies from the successful jump URL
* @param {string} jumpUrl
*/
static async getFinalCookies(jumpUrl) {
try {
// Prevent auto redirect to capture cookies
const response = await axios.get(jumpUrl, {
maxRedirects: 0,
validateStatus: status => status >= 200 && status < 400,
headers: {
'User-Agent': ChromeUA
}
});
// This might return 302 Found
return response.headers['set-cookie'];
} catch (error) {
console.error("Get Final Cookies Error", error);
return [];
}
}
}
class MiniProgramLoginSession {
static QUA = 'V1_HT5_QDT_0.70.2209190_x64_0_DEV_D';
/**
* Mini Program Presets
*/
static Presets = {
miniprogram: {
name: '小程序开发 (DevTools)',
description: 'QQ小程序开发者工具',
appid: '' // User provided
},
farm: {
name: 'QQ经典农场 (Farm)',
description: 'QQ经典农场小程序',
appid: '1112386029'
}
};
static getHeaders() {
return {
'qua': MiniProgramLoginSession.QUA,
'host': 'q.qq.com',
'accept': 'application/json',
'content-type': 'application/json',
'user-agent': ChromeUA
};
}
/**
* Request Login Code (for Mini Program DevTools)
*/
static async requestLoginCode() {
try {
const response = await axios.get('https://q.qq.com/ide/devtoolAuth/GetLoginCode', {
headers: this.getHeaders()
});
const { code, data } = response.data;
if (+code !== 0) {
throw new Error('获取登录码失败');
}
return {
code: data.code || '',
url: `https://h5.qzone.qq.com/qqq/code/${data.code}?_proxy=1&from=ide`
};
} catch (error) {
console.error('MP Request Login Code Error:', error);
throw error;
}
}
/**
* Query Status for Mini Program Login
* @param {string} code
*/
static async queryStatus(code) {
try {
const response = await axios.get(`https://q.qq.com/ide/devtoolAuth/syncScanSateGetTicket?code=${code}`, {
headers: this.getHeaders()
});
// If response is not OK (e.g. 404/500), return Error
if (response.status !== 200) {
return { status: 'Error' };
}
const { code: resCode, data } = response.data;
if (+resCode === 0) {
// data.ok: 1 = Success, 0 = Waiting/Scanning?
if (+data.ok !== 1) return { status: 'Wait' };
// User says uin is here
return { status: 'OK', ticket: data.ticket, uin: data.uin };
}
if (+resCode === -10003) return { status: 'Used' };
return { status: 'Error', msg: `Code: ${resCode}` };
} catch (error) {
console.error('MP Query Status Error:', error);
throw error;
}
}
/**
* Get Auth Code (Final step for MP login)
* @param {string} ticket
* @param {string} appid
*/
static async getAuthCode(ticket, appid) {
try {
const response = await axios.post('https://q.qq.com/ide/login', {
appid: appid,
ticket: ticket
}, {
headers: this.getHeaders()
});
if (response.status !== 200) return '';
const { code } = response.data;
return code || '';
} catch (error) {
console.error('MP Get Auth Code Error:', error);
return '';
}
}
}
module.exports = { QRLoginSession, MiniProgramLoginSession };

View File

@@ -0,0 +1,52 @@
const axios = require('axios');
/**
* Cookie handling utilities
*/
class CookieUtils {
static parse(cookieStr) {
if (!cookieStr) return {};
return cookieStr.split(';').reduce((acc, curr) => {
const [key, value] = curr.split('=');
if (key) acc[key.trim()] = value ? value.trim() : '';
return acc;
}, {});
}
static getValue(cookies, key) {
if (!cookies) return null;
if (Array.isArray(cookies)) cookies = cookies.join('; ');
const match = cookies.match(new RegExp(`(?:^|;\\s*)${key}=([^;]*)`));
return match ? match[1] : null;
}
static getUin(cookies) {
const uin = this.getValue(cookies, 'wxuin') || this.getValue(cookies, 'uin') || this.getValue(cookies, 'ptui_loginuin');
if (!uin) return null;
// Remove leading 'o' if present (common in QQ cookies like 'o123456')
return uin.replace(/^o0*/, '');
}
}
/**
* Hashing utilities for QQ login
*/
class HashUtils {
static hash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += (hash << 5) + str.charCodeAt(i);
}
return 2147483647 & hash;
}
static getGTk(pskey) {
let gtk = 5381;
for (let i = 0; i < pskey.length; i++) {
gtk += (gtk << 5) + pskey.charCodeAt(i);
}
return gtk & 0x7fffffff;
}
}
module.exports = { CookieUtils, HashUtils };

193
211/server/src/status.js Normal file
View File

@@ -0,0 +1,193 @@
/**
* 状态栏 - 在终端固定位置显示用户状态
*/
const { getLevelExpTable, getLevelExpProgress } = require('./gameConfig');
// ============ 状态数据 ============
const statusData = {
platform: 'qq',
name: '',
level: 0,
gold: 0,
exp: 0,
};
// ============ 状态栏高度 ============
const STATUS_LINES = 3; // 状态栏占用行数
const FREE_PROJECT_TIP = '本程序免费开源GitHub: https://github.com/linguo2625469/qq-farm-bot';
// ============ ANSI 转义码 ============
const ESC = '\x1b';
const SAVE_CURSOR = `${ESC}7`;
const RESTORE_CURSOR = `${ESC}8`;
const MOVE_TO = (row, col) => `${ESC}[${row};${col}H`;
const CLEAR_LINE = `${ESC}[2K`;
const SCROLL_REGION = (top, bottom) => `${ESC}[${top};${bottom}r`;
const RESET_SCROLL = `${ESC}[r`;
const BOLD = `${ESC}[1m`;
const RESET = `${ESC}[0m`;
const DIM = `${ESC}[2m`;
const CYAN = `${ESC}[36m`;
const YELLOW = `${ESC}[33m`;
const GREEN = `${ESC}[32m`;
const MAGENTA = `${ESC}[35m`;
// ============ 状态栏是否启用 ============
let statusEnabled = false;
let termRows = 24;
/**
* 初始化状态栏
*/
function initStatusBar() {
// 检测终端是否支持
if (!process.stdout.isTTY) {
return false;
}
termRows = process.stdout.rows || 24;
statusEnabled = true;
// 设置滚动区域,留出顶部状态栏空间
process.stdout.write(SCROLL_REGION(STATUS_LINES + 1, termRows));
// 移动光标到滚动区域
process.stdout.write(MOVE_TO(STATUS_LINES + 1, 1));
// 监听终端大小变化
process.stdout.on('resize', () => {
termRows = process.stdout.rows || 24;
process.stdout.write(SCROLL_REGION(STATUS_LINES + 1, termRows));
renderStatusBar();
});
// 初始渲染
renderStatusBar();
return true;
}
/**
* 清理状态栏(退出时调用)
*/
function cleanupStatusBar() {
if (!statusEnabled) return;
statusEnabled = false;
// 重置滚动区域
process.stdout.write(RESET_SCROLL);
// 清除状态栏
process.stdout.write(MOVE_TO(1, 1) + CLEAR_LINE);
process.stdout.write(MOVE_TO(2, 1) + CLEAR_LINE);
process.stdout.write(MOVE_TO(3, 1) + CLEAR_LINE);
}
/**
* 渲染状态栏
*/
function renderStatusBar() {
if (!statusEnabled) return;
const { platform, name, level, gold, exp } = statusData;
// 构建状态行
const platformStr = platform === 'wx' ? `${MAGENTA}微信${RESET}` : `${CYAN}QQ${RESET}`;
const nameStr = name ? `${BOLD}${name}${RESET}` : '未登录';
const levelStr = `${GREEN}Lv${level}${RESET}`;
const goldStr = `${YELLOW}金币:${gold}${RESET}`;
// 显示经验值
let expStr = '';
if (level > 0 && exp >= 0) {
const levelExpTable = getLevelExpTable();
if (levelExpTable) {
// 有配置表时显示当前等级进度
const progress = getLevelExpProgress(level, exp);
expStr = `${DIM}经验:${progress.current}/${progress.needed}${RESET}`;
} else {
// 没有配置表时只显示累计经验
expStr = `${DIM}经验:${exp}${RESET}`;
}
}
// 第一行:平台 | 昵称 | 等级 | 金币 | 经验
const line1 = `${platformStr} | ${nameStr} | ${levelStr} | ${goldStr}${expStr ? ' | ' + expStr : ''}`;
// 第二行:固定提醒
const line2 = `${DIM}${FREE_PROJECT_TIP}${RESET}`;
// 第三行:分隔线
const width = process.stdout.columns || 80;
const line3 = `${DIM}${'─'.repeat(Math.min(width, 80))}${RESET}`;
// 保存光标位置
process.stdout.write(SAVE_CURSOR);
// 移动到第一行并清除
process.stdout.write(MOVE_TO(1, 1) + CLEAR_LINE + line1);
// 移动到第二行并清除
process.stdout.write(MOVE_TO(2, 1) + CLEAR_LINE + line2);
// 移动到第三行并清除
process.stdout.write(MOVE_TO(3, 1) + CLEAR_LINE + line3);
// 恢复光标位置
process.stdout.write(RESTORE_CURSOR);
}
/**
* 更新状态数据并刷新显示
*/
function updateStatus(data) {
let changed = false;
for (const key of Object.keys(data)) {
if (statusData[key] !== data[key]) {
statusData[key] = data[key];
changed = true;
}
}
if (changed && statusEnabled) {
renderStatusBar();
}
}
/**
* 设置平台
*/
function setStatusPlatform(platform) {
updateStatus({ platform });
}
/**
* 从登录数据更新状态
*/
function updateStatusFromLogin(basic) {
updateStatus({
name: basic.name || statusData.name,
level: basic.level || statusData.level,
gold: basic.gold || statusData.gold,
exp: basic.exp || statusData.exp,
});
}
/**
* 更新金币
*/
function updateStatusGold(gold) {
updateStatus({ gold });
}
/**
* 更新等级和经验
*/
function updateStatusLevel(level, exp) {
const data = { level };
if (exp !== undefined) data.exp = exp;
updateStatus(data);
}
module.exports = {
initStatusBar,
cleanupStatusBar,
updateStatus,
setStatusPlatform,
updateStatusFromLogin,
updateStatusGold,
updateStatusLevel,
statusData,
};

181
211/server/src/task.js Normal file
View File

@@ -0,0 +1,181 @@
/**
* 任务系统 - 自动领取任务奖励
*/
const { types } = require('./proto');
const { sendMsgAsync, networkEvents } = require('./network');
const { toLong, toNum, log, logWarn, sleep } = require('./utils');
// ============ 任务 API ============
async function getTaskInfo() {
const body = types.TaskInfoRequest.encode(types.TaskInfoRequest.create({})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'TaskInfo', body);
return types.TaskInfoReply.decode(replyBody);
}
async function claimTaskReward(taskId, doShared = false) {
const body = types.ClaimTaskRewardRequest.encode(types.ClaimTaskRewardRequest.create({
id: toLong(taskId),
do_shared: doShared,
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'ClaimTaskReward', body);
return types.ClaimTaskRewardReply.decode(replyBody);
}
async function batchClaimTaskReward(taskIds, doShared = false) {
const body = types.BatchClaimTaskRewardRequest.encode(types.BatchClaimTaskRewardRequest.create({
ids: taskIds.map(id => toLong(id)),
do_shared: doShared,
})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.taskpb.TaskService', 'BatchClaimTaskReward', body);
return types.BatchClaimTaskRewardReply.decode(replyBody);
}
// ============ 任务分析 ============
/**
* 分析任务列表,找出可领取的任务
*/
function analyzeTaskList(tasks) {
const claimable = [];
for (const task of tasks) {
const id = toNum(task.id);
const progress = toNum(task.progress);
const totalProgress = toNum(task.total_progress);
const isClaimed = task.is_claimed;
const isUnlocked = task.is_unlocked;
const shareMultiple = toNum(task.share_multiple);
// 可领取条件: 已解锁 + 未领取 + 进度完成
if (isUnlocked && !isClaimed && progress >= totalProgress && totalProgress > 0) {
claimable.push({
id,
desc: task.desc || `任务#${id}`,
shareMultiple,
rewards: task.rewards || [],
});
}
}
return claimable;
}
/**
* 计算奖励摘要
*/
function getRewardSummary(items) {
const summary = [];
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
// 常见物品ID: 1=金币, 2=经验
if (id === 1) summary.push(`金币${count}`);
else if (id === 2) summary.push(`经验${count}`);
else summary.push(`物品#${id}x${count}`);
}
return summary.join('/');
}
// ============ 自动领取 ============
/**
* 检查并领取所有可领取的任务奖励
*/
async function checkAndClaimTasks() {
try {
const reply = await getTaskInfo();
if (!reply.task_info) return;
const taskInfo = reply.task_info;
const allTasks = [
...(taskInfo.growth_tasks || []),
...(taskInfo.daily_tasks || []),
...(taskInfo.tasks || []),
];
const claimable = analyzeTaskList(allTasks);
if (claimable.length === 0) return;
log('任务', `发现 ${claimable.length} 个可领取任务`);
for (const task of claimable) {
try {
// 如果有分享翻倍,使用翻倍领取
const useShare = task.shareMultiple > 1;
const multipleStr = useShare ? ` (${task.shareMultiple}倍)` : '';
const claimReply = await claimTaskReward(task.id, useShare);
const items = claimReply.items || [];
const rewardStr = items.length > 0 ? getRewardSummary(items) : '无';
log('任务', `领取: ${task.desc}${multipleStr}${rewardStr}`);
await sleep(300);
} catch (e) {
logWarn('任务', `领取失败 #${task.id}: ${e.message}`);
}
}
} catch (e) {
// 静默失败
}
}
/**
* 处理任务状态变化推送
*/
function onTaskInfoNotify(taskInfo) {
if (!taskInfo) return;
const allTasks = [
...(taskInfo.growth_tasks || []),
...(taskInfo.daily_tasks || []),
...(taskInfo.tasks || []),
];
const claimable = analyzeTaskList(allTasks);
if (claimable.length === 0) return;
// 有可领取任务,延迟后自动领取
log('任务', `${claimable.length} 个任务可领取,准备自动领取...`);
setTimeout(() => claimTasksFromList(claimable), 1000);
}
/**
* 从任务列表领取奖励
*/
async function claimTasksFromList(claimable) {
for (const task of claimable) {
try {
const useShare = task.shareMultiple > 1;
const multipleStr = useShare ? ` (${task.shareMultiple}倍)` : '';
const claimReply = await claimTaskReward(task.id, useShare);
const items = claimReply.items || [];
const rewardStr = items.length > 0 ? getRewardSummary(items) : '无';
log('任务', `领取: ${task.desc}${multipleStr}${rewardStr}`);
await sleep(300);
} catch (e) {
logWarn('任务', `领取失败 #${task.id}: ${e.message}`);
}
}
}
// ============ 初始化 ============
function initTaskSystem() {
// 监听任务状态变化推送
networkEvents.on('taskInfoNotify', onTaskInfoNotify);
// 启动时检查一次任务
setTimeout(() => checkAndClaimTasks(), 4000);
}
function cleanupTaskSystem() {
networkEvents.off('taskInfoNotify', onTaskInfoNotify);
}
module.exports = {
checkAndClaimTasks,
initTaskSystem,
cleanupTaskSystem,
};

View File

@@ -0,0 +1,475 @@
const fs = require('fs');
const path = require('path');
const { log } = require('./utils');
require('dotenv').config({ path: path.join(__dirname, '../../.env') });
const DATA_DIR = path.join(__dirname, '../data');
const DATA_FILE = path.join(DATA_DIR, 'users.json');
const SQLITE_FILE = path.join(DATA_DIR, 'users.db');
const isProduction = process.env.NODE_ENV === 'production';
let mysqlPool = null;
let sqliteDb = null;
let sqliteRun = null;
let sqliteAll = null;
let sqliteReady = null;
if (isProduction) {
const mysql = require('mysql2/promise');
mysqlPool = mysql.createPool({
host: process.env.MYSQL_HOST || 'localhost',
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || 'root',
database: process.env.MYSQL_DB || 'game_account_db',
port: process.env.MYSQL_PORT || 3306,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Init Table
(async () => {
try {
const connection = await mysqlPool.getConnection();
await connection.query(`
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255),
code TEXT,
auth_provider VARCHAR(50) DEFAULT 'local',
auth_id VARCHAR(255),
created_at BIGINT
)
`);
connection.release();
log('UserManager', 'MySQL initialized.');
} catch (err) {
log('UserManager', `MySQL Init Error: ${err.message}`);
}
})();
}
if (!isProduction) {
const sqlite3 = require('sqlite3');
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
sqliteDb = new sqlite3.Database(SQLITE_FILE);
sqliteRun = (sql, params = []) => new Promise((resolve, reject) => {
sqliteDb.run(sql, params, function (err) {
if (err) reject(err);
else resolve(this);
});
});
sqliteAll = (sql, params = []) => new Promise((resolve, reject) => {
sqliteDb.all(sql, params, (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
sqliteReady = sqliteRun(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password TEXT,
code TEXT,
auth_provider TEXT DEFAULT 'local',
auth_id TEXT,
created_at INTEGER
)
`).then(() => {
log('UserManager', 'SQLite initialized.');
}).catch((err) => {
log('UserManager', `SQLite Init Error: ${err.message}`);
});
}
class UserManager {
constructor() {
this.users = new Map(); // email -> user (Cache)
this.useMySQL = isProduction && !!mysqlPool;
this.useSQLite = !this.useMySQL && !!sqliteDb;
this.extraReady = this.initExtraTables();
this.load();
}
async load() {
if (this.useMySQL) {
try {
const [rows] = await mysqlPool.query('SELECT * FROM users');
rows.forEach(u => this.users.set(u.email, u));
log('UserManager', `Loaded ${rows.length} users from MySQL.`);
} catch (e) {
log('UserManager', `Failed to load users from MySQL: ${e.message}`);
}
} else if (this.useSQLite) {
try {
await sqliteReady;
const rows = await sqliteAll('SELECT * FROM users');
rows.forEach(u => this.users.set(u.email, u));
log('UserManager', `Loaded ${rows.length} users from SQLite.`);
} catch (e) {
log('UserManager', `Failed to load users from SQLite: ${e.message}`);
}
} else {
try {
if (fs.existsSync(DATA_FILE)) {
const data = fs.readFileSync(DATA_FILE, 'utf8');
const json = JSON.parse(data);
if (Array.isArray(json)) {
json.forEach(u => this.users.set(u.email, u));
}
log('UserManager', `Loaded ${this.users.size} users from JSON.`);
}
} catch (e) {
log('UserManager', `Failed to load users from JSON: ${e.message}`);
}
}
}
async save(user) {
const createdAt = user.createdAt || user.created_at || Date.now();
if (this.useMySQL) {
try {
await mysqlPool.query(
`INSERT INTO users (email, password, code, auth_provider, auth_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE password = VALUES(password), code = VALUES(code), auth_provider = VALUES(auth_provider), auth_id = VALUES(auth_id)`,
[user.email, user.password, user.code, user.auth_provider || 'local', user.auth_id || null, createdAt]
);
} catch (e) {
log('UserManager', `Failed to save user to MySQL: ${e.message}`);
}
} else if (this.useSQLite) {
try {
await sqliteReady;
await sqliteRun(
`INSERT INTO users (email, password, code, auth_provider, auth_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(email) DO UPDATE SET password = excluded.password, code = excluded.code, auth_provider = excluded.auth_provider, auth_id = excluded.auth_id`,
[user.email, user.password, user.code, user.auth_provider || 'local', user.auth_id || null, createdAt]
);
} catch (e) {
log('UserManager', `Failed to save user to SQLite: ${e.message}`);
}
} else {
try {
const data = JSON.stringify(Array.from(this.users.values()), null, 2);
fs.writeFileSync(DATA_FILE, data, 'utf8');
} catch (e) {
log('UserManager', `Failed to save users to JSON: ${e.message}`);
}
}
}
async initExtraTables() {
if (this.useMySQL) {
try {
await mysqlPool.query(`
CREATE TABLE IF NOT EXISTS bot_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
tag VARCHAR(50),
msg TEXT,
type VARCHAR(20),
time BIGINT,
INDEX idx_email_time (email, time)
)
`);
await mysqlPool.query(`
CREATE TABLE IF NOT EXISTS leaderboard_unlucky (
email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
count BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (email, name)
)
`);
} catch (e) {
log('UserManager', `Failed to init extra tables: ${e.message}`);
}
return;
}
if (this.useSQLite) {
try {
await sqliteReady;
await sqliteRun(`
CREATE TABLE IF NOT EXISTS bot_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
tag TEXT,
msg TEXT,
type TEXT,
time INTEGER
)
`);
await sqliteRun(`
CREATE TABLE IF NOT EXISTS leaderboard_unlucky (
email TEXT NOT NULL,
name TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (email, name)
)
`);
} catch (e) {
log('UserManager', `Failed to init extra tables: ${e.message}`);
}
}
}
normalizeLogMessage(msg) {
if (typeof msg === 'string') return msg;
if (msg === null || msg === undefined) return '';
if (typeof msg === 'object') {
try {
return JSON.stringify(msg);
} catch {
return String(msg);
}
}
return String(msg);
}
async saveBotLog(email, logEntry) {
if (!email || !logEntry) return;
await this.extraReady;
const tag = logEntry.tag || 'System';
const msg = this.normalizeLogMessage(logEntry.msg);
const type = logEntry.type || 'info';
const time = Number(logEntry.time) || Date.now();
const limit = 500;
if (this.useMySQL) {
try {
await mysqlPool.query(
`INSERT INTO bot_logs (email, tag, msg, type, time) VALUES (?, ?, ?, ?, ?)`,
[email, tag, msg, type, time]
);
await mysqlPool.query(
`DELETE FROM bot_logs WHERE email = ? AND id NOT IN (
SELECT id FROM (
SELECT id FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?
) t
)`,
[email, email, limit]
);
} catch (e) {
log('UserManager', `Failed to save bot log: ${e.message}`);
}
return;
}
if (this.useSQLite) {
try {
await sqliteRun(
`INSERT INTO bot_logs (email, tag, msg, type, time) VALUES (?, ?, ?, ?, ?)`,
[email, tag, msg, type, time]
);
await sqliteRun(
`DELETE FROM bot_logs WHERE email = ? AND id NOT IN (
SELECT id FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?
)`,
[email, email, limit]
);
} catch (e) {
log('UserManager', `Failed to save bot log: ${e.message}`);
}
}
}
async getBotLogs(email, limit = 200) {
if (!email) return [];
await this.extraReady;
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 500) : 200;
if (this.useMySQL) {
try {
const [rows] = await mysqlPool.query(
`SELECT id, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`,
[email, safeLimit]
);
return rows.slice().reverse().map(row => ({
id: String(row.id),
tag: row.tag || 'System',
msg: row.msg || '',
type: row.type || 'info',
time: Number(row.time) || Date.now()
}));
} catch (e) {
log('UserManager', `Failed to load bot logs: ${e.message}`);
return [];
}
}
if (this.useSQLite) {
try {
const rows = await sqliteAll(
`SELECT id, tag, msg, type, time FROM bot_logs WHERE email = ? ORDER BY id DESC LIMIT ?`,
[email, safeLimit]
);
return rows.slice().reverse().map(row => ({
id: String(row.id),
tag: row.tag || 'System',
msg: row.msg || '',
type: row.type || 'info',
time: Number(row.time) || Date.now()
}));
} catch (e) {
log('UserManager', `Failed to load bot logs: ${e.message}`);
return [];
}
}
return [];
}
async saveUnlucky(email, name, count) {
if (!email || !name || !Number.isFinite(count) || count <= 0) return;
await this.extraReady;
if (this.useMySQL) {
try {
await mysqlPool.query(
`INSERT INTO leaderboard_unlucky (email, name, count)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE count = count + VALUES(count)`,
[email, name, count]
);
} catch (e) {
log('UserManager', `Failed to save leaderboard: ${e.message}`);
}
return;
}
if (this.useSQLite) {
try {
await sqliteRun(
`INSERT INTO leaderboard_unlucky (email, name, count)
VALUES (?, ?, ?)
ON CONFLICT(email, name) DO UPDATE SET count = count + excluded.count`,
[email, name, count]
);
} catch (e) {
log('UserManager', `Failed to save leaderboard: ${e.message}`);
}
}
}
async loadUnluckyBoards() {
await this.extraReady;
const result = new Map();
if (this.useMySQL) {
try {
const [rows] = await mysqlPool.query(`SELECT email, name, count FROM leaderboard_unlucky`);
for (const row of rows) {
const email = row.email;
const name = row.name;
const count = Number(row.count) || 0;
if (!email || !name || count <= 0) continue;
let board = result.get(email);
if (!board) {
board = new Map();
result.set(email, board);
}
board.set(name, count);
}
} catch (e) {
log('UserManager', `Failed to load leaderboard: ${e.message}`);
}
return result;
}
if (this.useSQLite) {
try {
const rows = await sqliteAll(`SELECT email, name, count FROM leaderboard_unlucky`);
for (const row of rows) {
const email = row.email;
const name = row.name;
const count = Number(row.count) || 0;
if (!email || !name || count <= 0) continue;
let board = result.get(email);
if (!board) {
board = new Map();
result.set(email, board);
}
board.set(name, count);
}
} catch (e) {
log('UserManager', `Failed to load leaderboard: ${e.message}`);
}
}
return result;
}
/**
* Get user by email
* @param {string} email
*/
getUser(email) {
return this.users.get(email);
}
/**
* Create or update user
* @param {string} email
* @param {string} password
*/
async register(email, password, provider = 'local', authId = null) {
if (this.users.has(email) && provider === 'local') {
throw new Error('User already exists');
}
let user = this.users.get(email);
if (user) {
// Update existing user (e.g. linking OAuth)
if (provider !== 'local') {
user.auth_provider = provider;
user.auth_id = authId;
await this.save(user);
}
return user;
}
user = {
email,
password, // In production, hash this!
code: '',
auth_provider: provider,
auth_id: authId,
createdAt: Date.now()
};
this.users.set(email, user);
await this.save(user);
return user;
}
/**
* Validate login
* @param {string} email
* @param {string} password
*/
login(email, password) {
const user = this.users.get(email);
if (!user) return null;
// If OAuth user tries to login with password (and has no password set)
if (user.auth_provider !== 'local' && !user.password) {
return null;
}
if (user.password !== password) {
return null;
}
return user;
}
/**
* Update user's game code
* @param {string} email
* @param {string} code
*/
async updateCode(email, code) {
const user = this.users.get(email);
if (user) {
user.code = code;
await this.save(user);
return true;
}
return false;
}
}
module.exports = new UserManager();

90
211/server/src/utils.js Normal file
View File

@@ -0,0 +1,90 @@
/**
* 通用工具函数
*/
const Long = require('long');
const { RUNTIME_HINT_MASK, RUNTIME_HINT_DATA } = require('./config');
// ============ 服务器时间状态 ============
let serverTimeMs = 0;
let localTimeAtSync = 0;
// ============ 类型转换 ============
function toLong(val) {
return Long.fromValue(val);
}
function toNum(val) {
if (Long.isLong(val)) return val.toNumber();
return val || 0;
}
// ============ 时间相关 ============
function now() {
return new Date().toLocaleTimeString();
}
/** 获取当前推算的服务器时间(秒) */
function getServerTimeSec() {
if (!serverTimeMs) return Math.floor(Date.now() / 1000);
const elapsed = Date.now() - localTimeAtSync;
return Math.floor((serverTimeMs + elapsed) / 1000);
}
/** 同步服务器时间 */
function syncServerTime(ms) {
serverTimeMs = ms;
localTimeAtSync = Date.now();
}
/**
* 将时间戳归一化为秒级
* 大于 1e12 认为是毫秒级,转换为秒级
*/
function toTimeSec(val) {
const n = toNum(val);
if (n <= 0) return 0;
if (n > 1e12) return Math.floor(n / 1000);
return n;
}
// ============ 日志 ============
function log(tag, msg) {
console.log(`[${now()}] [${tag}] ${msg}`);
}
function logWarn(tag, msg) {
console.log(`[${now()}] [${tag}] ⚠ ${msg}`);
}
// ============ 异步工具 ============
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
let hintPrinted = false;
function decodeRuntimeHint() {
return String.fromCharCode(...RUNTIME_HINT_DATA.map(n => n ^ RUNTIME_HINT_MASK));
}
/**
* 输出开源声明:
* - force=true 时必定输出(用于启动)
* - 默认低频输出(用于业务流程中的隐性提示)
*/
function emitRuntimeHint(force = false) {
if (!force) {
// 约 3.3% 概率,且同一次进程最多输出 2 次
if (Math.random() > 0.033) return;
if (hintPrinted && Math.random() > 0.2) return;
}
log('声明', decodeRuntimeHint());
hintPrinted = true;
}
module.exports = {
toLong, toNum, now,
getServerTimeSec, syncServerTime, toTimeSec,
log, logWarn, sleep,
emitRuntimeHint,
};

173
211/server/src/warehouse.js Normal file
View File

@@ -0,0 +1,173 @@
/**
* 仓库系统 - 自动出售果实
* 协议说明BagReply 使用 item_bagItemBagitem_bag.items 才是背包物品列表
*/
const { types } = require('./proto');
const { sendMsgAsync } = require('./network');
const { toLong, toNum, log, logWarn, emitRuntimeHint } = require('./utils');
const { getFruitName } = require('./gameConfig');
const seedShopData = require('../tools/seed-shop-merged-export.json');
// 游戏内金币和点券的物品 ID (GlobalData.GodItemId / DiamondItemId)
const GOLD_ITEM_ID = 1001;
const FRUIT_ID_SET = new Set(
((seedShopData && seedShopData.rows) || [])
.map(row => Number(row.fruitId))
.filter(Number.isFinite)
);
let sellTimer = null;
let sellInterval = 60000;
function isFruitIdBySeedData(id) {
return FRUIT_ID_SET.has(toNum(id));
}
/**
* 从 SellReply 中提取获得的金币数量
* 新版 SellReply 返回 get_items (repeated Item),其中 id=1001 为金币
*/
function extractGold(sellReply) {
if (sellReply.get_items && sellReply.get_items.length > 0) {
for (const item of sellReply.get_items) {
const id = toNum(item.id);
if (id === GOLD_ITEM_ID) {
return toNum(item.count);
}
}
return 0;
}
if (sellReply.gold !== undefined && sellReply.gold !== null) {
return toNum(sellReply.gold);
}
return 0;
}
async function getBag() {
const body = types.BagRequest.encode(types.BagRequest.create({})).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.itempb.ItemService', 'Bag', body);
return types.BagReply.decode(replyBody);
}
/**
* 将 item 转为 Sell 请求所需格式id/count/uid 保留 Long 或转成 Long与游戏一致
*/
function toSellItem(item) {
const id = item.id != null ? toLong(item.id) : undefined;
const count = item.count != null ? toLong(item.count) : undefined;
const uid = item.uid != null ? toLong(item.uid) : undefined;
return { id, count, uid };
}
async function sellItems(items) {
const payload = items.map(toSellItem);
const body = types.SellRequest.encode(types.SellRequest.create({ items: payload })).finish();
const { body: replyBody } = await sendMsgAsync('gamepb.itempb.ItemService', 'Sell', body);
return types.SellReply.decode(replyBody);
}
/**
* 从 BagReply 取出物品列表(兼容 item_bag 与旧版 items
*/
function getBagItems(bagReply) {
if (bagReply.item_bag && bagReply.item_bag.items && bagReply.item_bag.items.length)
return bagReply.item_bag.items;
return bagReply.items || [];
}
async function sellAllFruits() {
try {
const bagReply = await getBag();
const items = getBagItems(bagReply);
const toSell = [];
const names = [];
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
const uid = item.uid ? toNum(item.uid) : 0;
if (isFruitIdBySeedData(id) && count > 0) {
if (uid === 0) continue; // 跳过无效格子
toSell.push(item);
names.push(`${getFruitName(id)}x${count}`);
}
}
if (toSell.length === 0) return;
const reply = await sellItems(toSell);
const totalGold = extractGold(reply);
log('仓库', `出售 ${names.join(', ')},获得 ${totalGold} 金币`);
emitRuntimeHint(false);
} catch (e) {
logWarn('仓库', `出售失败: ${e.message}`);
}
}
async function debugSellFruits() {
try {
log('仓库', '正在检查背包...');
const bagReply = await getBag();
const items = getBagItems(bagReply);
log('仓库', `背包共 ${items.length} 种物品`);
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
const isFruit = isFruitIdBySeedData(id);
if (isFruit) {
const name = getFruitName(id);
log('仓库', ` [果实] ${name}(${id}) x${count}`);
}
}
const toSell = [];
for (const item of items) {
const id = toNum(item.id);
const count = toNum(item.count);
if (isFruitIdBySeedData(id) && count > 0)
toSell.push(item);
}
if (toSell.length === 0) {
log('仓库', '没有果实可出售');
return;
}
log('仓库', `准备出售 ${toSell.length} 种果实...`);
const reply = await sellItems(toSell);
const totalGold = extractGold(reply);
log('仓库', `出售完成,共获得 ${totalGold} 金币`);
emitRuntimeHint(false);
} catch (e) {
logWarn('仓库', `调试出售失败: ${e.message}`);
console.error(e);
}
}
function startSellLoop(interval = 60000) {
if (sellTimer) return;
sellInterval = interval;
setTimeout(() => {
sellAllFruits();
sellTimer = setInterval(() => sellAllFruits(), sellInterval);
}, 10000);
}
function stopSellLoop() {
if (sellTimer) {
clearInterval(sellTimer);
sellTimer = null;
}
}
module.exports = {
getBag,
sellItems,
sellAllFruits,
debugSellFruits,
getBagItems,
startSellLoop,
stopSellLoop,
};

View File

@@ -0,0 +1,474 @@
/**
* 基于 tools/seed-shop-merged-export.json 计算经验收益率
*
* 规则:
* 1) 每次收获经验 = exp新版已去除铲地+1经验
* 2) 种植速度:
* - 不施肥2 秒种 18 块地 => 9 块/秒
* - 普通肥2 秒种 12 块地 => 6 块/秒
* 3) 普通肥:直接减少一个生长阶段(按 Plant.json 的 grow_phases 取首个非0阶段时长
*
* 用法:
* node tools/calc-exp-yield.js
* node tools/calc-exp-yield.js --lands 18 --level 27
* node tools/calc-exp-yield.js --input tools/seed-shop-merged-export.json
*
* 运行时调用:
* const { getPlantingRecommendation } = require('../tools/calc-exp-yield');
* const rec = getPlantingRecommendation(27, 18);
*/
const fs = require('fs');
const path = require('path');
const DEFAULT_INPUT = path.join(__dirname, 'seed-shop-merged-export.json');
const PLANT_CONFIG_PATH = path.join(__dirname, '..', 'gameConfig', 'Plant.json');
const DEFAULT_OUT_JSON = path.join(__dirname, 'exp-yield-result.json');
const DEFAULT_OUT_CSV = path.join(__dirname, 'exp-yield-result.csv');
const DEFAULT_OUT_TXT = path.join(__dirname, 'exp-yield-summary.txt');
const NO_FERT_PLANTS_PER_2_SEC = 18;
const NORMAL_FERT_PLANTS_PER_2_SEC = 12;
const NO_FERT_PLANT_SPEED_PER_SEC = NO_FERT_PLANTS_PER_2_SEC / 2; // 9 块/秒
const NORMAL_FERT_PLANT_SPEED_PER_SEC = NORMAL_FERT_PLANTS_PER_2_SEC / 2; // 6 块/秒
function toNum(v, fallback = 0) {
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function parseArgs(argv) {
const opts = {
input: DEFAULT_INPUT,
outJson: DEFAULT_OUT_JSON,
outCsv: DEFAULT_OUT_CSV,
outTxt: DEFAULT_OUT_TXT,
lands: 18,
level: null,
top: 20,
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--input' && argv[i + 1]) opts.input = argv[++i];
else if (a === '--out-json' && argv[i + 1]) opts.outJson = argv[++i];
else if (a === '--out-csv' && argv[i + 1]) opts.outCsv = argv[++i];
else if (a === '--out-txt' && argv[i + 1]) opts.outTxt = argv[++i];
else if (a === '--lands' && argv[i + 1]) opts.lands = Math.max(1, Math.floor(toNum(argv[++i], 18)));
else if (a === '--level' && argv[i + 1]) opts.level = Math.max(1, Math.floor(toNum(argv[++i], 1)));
else if (a === '--top' && argv[i + 1]) opts.top = Math.max(1, Math.floor(toNum(argv[++i], 20)));
else if (a === '--help' || a === '-h') {
printHelp();
process.exit(0);
}
}
return opts;
}
function printHelp() {
console.log('Usage: node tools/calc-exp-yield.js [options]');
console.log('');
console.log('Options:');
console.log(' --input <path> 输入 JSON 文件路径');
console.log(' --lands <n> 地块数(默认 18');
console.log(' --level <n> 指定账号等级,输出该等级可用最优作物');
console.log(' --top <n> 摘要 Top 数量(默认 20');
console.log(' --out-json <path> 输出 JSON 路径');
console.log(' --out-csv <path> 输出 CSV 路径');
console.log(' --out-txt <path> 输出 TXT 路径');
}
function readSeeds(inputPath) {
const text = fs.readFileSync(inputPath, 'utf8');
const data = JSON.parse(text);
if (Array.isArray(data)) return data;
if (data && Array.isArray(data.rows)) return data.rows;
if (data && Array.isArray(data.seeds)) return data.seeds;
throw new Error('无法识别输入数据格式,需要数组或 rows/seeds 字段');
}
function parseGrowPhases(growPhases) {
if (!growPhases || typeof growPhases !== 'string') return [];
return growPhases
.split(';')
.map(x => x.trim())
.filter(Boolean)
.map(seg => {
const parts = seg.split(':');
return parts.length >= 2 ? toNum(parts[1], 0) : 0;
})
.filter(sec => sec > 0);
}
function loadSeedPhaseReduceMap() {
const text = fs.readFileSync(PLANT_CONFIG_PATH, 'utf8');
const rows = JSON.parse(text);
if (!Array.isArray(rows)) {
throw new Error(`Plant 配置格式异常: ${PLANT_CONFIG_PATH}`);
}
const map = new Map();
for (const p of rows) {
const seedId = toNum(p.seed_id, 0);
if (seedId <= 0 || map.has(seedId)) continue;
const phases = parseGrowPhases(p.grow_phases);
if (phases.length === 0) continue;
map.set(seedId, phases[0]); // 普通肥减少一个阶段:以首个阶段时长为准
}
return map;
}
function calcEffectiveGrowTime(growSec, seedId, seedPhaseReduceMap) {
const reduce = toNum(seedPhaseReduceMap.get(seedId), 0);
if (reduce <= 0) return growSec;
return Math.max(1, growSec - reduce);
}
function formatSec(sec) {
const s = Math.max(0, Math.round(sec));
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
const r = s % 60;
if (m < 60) return r > 0 ? `${m}m${r}s` : `${m}m`;
const h = Math.floor(m / 60);
const mm = m % 60;
return r > 0 ? `${h}h${mm}m${r}s` : `${h}h${mm}m`;
}
function csvCell(v) {
const s = v == null ? '' : String(v);
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
}
function buildRows(rawSeeds, lands, seedPhaseReduceMap) {
const plantSecondsNoFert = lands / NO_FERT_PLANT_SPEED_PER_SEC;
const plantSecondsNormalFert = lands / NORMAL_FERT_PLANT_SPEED_PER_SEC;
const rows = [];
let skipped = 0;
let missingPhaseReduceCount = 0;
for (const s of rawSeeds) {
const seedId = toNum(s.seedId || s.seed_id);
const name = s.name || `seed_${seedId}`;
const requiredLevel = toNum(s.requiredLevel || s.required_level || 1, 1);
const price = toNum(s.price, 0);
const expHarvest = toNum(s.exp, 0);
const growTimeSec = toNum(s.growTimeSec || s.growTime || s.grow_time || 0, 0);
if (seedId <= 0 || growTimeSec <= 0) {
skipped++;
continue;
}
const expPerCycle = expHarvest;
const reduceSec = toNum(seedPhaseReduceMap.get(seedId), 0);
if (reduceSec <= 0) missingPhaseReduceCount++;
const growTimeNormalFert = calcEffectiveGrowTime(growTimeSec, seedId, seedPhaseReduceMap);
// 整个农场一轮 = 生长时间 + 本轮全部地块种植耗时
const cycleSecNoFert = growTimeSec + plantSecondsNoFert;
const cycleSecNormalFert = growTimeNormalFert + plantSecondsNormalFert;
const farmExpPerHourNoFert = (lands * expPerCycle / cycleSecNoFert) * 3600;
const farmExpPerHourNormalFert = (lands * expPerCycle / cycleSecNormalFert) * 3600;
const gainPercent = farmExpPerHourNoFert > 0
? ((farmExpPerHourNormalFert - farmExpPerHourNoFert) / farmExpPerHourNoFert) * 100
: 0;
const expPerGoldSeed = price > 0 ? expPerCycle / price : 0;
rows.push({
seedId,
goodsId: toNum(s.goodsId || s.goods_id),
plantId: toNum(s.plantId || s.plant_id),
name,
requiredLevel,
unlocked: !!s.unlocked,
price,
expHarvest,
expPerCycle,
growTimeSec,
growTimeStr: s.growTimeStr || formatSec(growTimeSec),
normalFertReduceSec: reduceSec,
growTimeNormalFert,
growTimeNormalFertStr: formatSec(growTimeNormalFert),
cycleSecNoFert,
cycleSecNormalFert,
farmExpPerHourNoFert,
farmExpPerHourNormalFert,
farmExpPerDayNoFert: farmExpPerHourNoFert * 24,
farmExpPerDayNormalFert: farmExpPerHourNormalFert * 24,
gainPercent,
expPerGoldSeed,
fruitId: toNum(s?.fruit?.id || s.fruitId),
fruitCount: toNum(s?.fruit?.count || s.fruitCount),
});
}
return { rows, skipped, plantSecondsNoFert, plantSecondsNormalFert, missingPhaseReduceCount };
}
function pickTop(rows, key, topN) {
return [...rows]
.sort((a, b) => b[key] - a[key])
.slice(0, topN);
}
function buildBestByLevel(rows) {
const maxLevel = rows.reduce((m, r) => Math.max(m, r.requiredLevel), 1);
const result = [];
for (let lv = 1; lv <= maxLevel; lv++) {
// 按用户指定等级做理论可种分析,不受商店 unlocked 状态影响
const available = rows.filter(r => r.requiredLevel <= lv);
if (available.length === 0) continue;
const bestNo = pickTop(available, 'farmExpPerHourNoFert', 1)[0];
const bestFert = pickTop(available, 'farmExpPerHourNormalFert', 1)[0];
result.push({
level: lv,
bestNoFert: {
seedId: bestNo.seedId,
name: bestNo.name,
expPerHour: Number(bestNo.farmExpPerHourNoFert.toFixed(2)),
},
bestNormalFert: {
seedId: bestFert.seedId,
name: bestFert.name,
expPerHour: Number(bestFert.farmExpPerHourNormalFert.toFixed(2)),
},
});
}
return result;
}
function writeJson(outPath, payload) {
fs.writeFileSync(outPath, JSON.stringify(payload, null, 2), 'utf8');
}
function writeCsv(outPath, rows) {
const headers = [
'seedId',
'name',
'requiredLevel',
'price',
'expHarvest',
'expPerCycle',
'growTimeSec',
'growTimeNormalFert',
'cycleSecNoFert',
'cycleSecNormalFert',
'farmExpPerHourNoFert',
'farmExpPerHourNormalFert',
'farmExpPerDayNoFert',
'farmExpPerDayNormalFert',
'gainPercent',
'expPerGoldSeed',
];
const lines = [headers.join(',')];
for (const r of rows) {
lines.push(headers.map(h => csvCell(r[h])).join(','));
}
fs.writeFileSync(outPath, `${lines.join('\n')}\n`, 'utf8');
}
function writeSummaryTxt(outPath, opts, meta, topNo, topFert, levelInfo) {
const lines = [];
lines.push('经验收益率分析结果');
lines.push('');
lines.push(`数据源: ${meta.input}`);
lines.push(`导出时间: ${new Date().toISOString()}`);
lines.push(`地块数: ${opts.lands}`);
lines.push(`种植速度(不施肥): ${NO_FERT_PLANTS_PER_2_SEC}块/${2}s (${NO_FERT_PLANT_SPEED_PER_SEC}块/s)`);
lines.push(`种植速度(普通肥): ${NORMAL_FERT_PLANTS_PER_2_SEC}块/${2}s (${NORMAL_FERT_PLANT_SPEED_PER_SEC}块/s)`);
lines.push(`整场种植耗时(不施肥): ${formatSec(meta.plantSecondsNoFert)}`);
lines.push(`整场种植耗时(普通肥): ${formatSec(meta.plantSecondsNormalFert)}`);
lines.push(`普通肥规则: 直接减少一个生长阶段(按 Plant.json 的首个阶段时长)`);
lines.push(`缺少阶段配置的种子数: ${meta.missingPhaseReduceCount}`);
lines.push('');
lines.push(`Top ${topNo.length}(不施肥,按每小时经验)`);
lines.push('排名 | 名称 | Lv需 | 生长 | 单轮经验 | 每小时经验');
topNo.forEach((r, i) => {
lines.push(
`${String(i + 1).padStart(2)} | ${r.name} | ${r.requiredLevel} | ${r.growTimeStr} | ${r.expPerCycle} | ${r.farmExpPerHourNoFert.toFixed(2)}`
);
});
lines.push('');
lines.push(`Top ${topFert.length}(普通肥,按每小时经验)`);
lines.push('排名 | 名称 | Lv需 | 肥后生长 | 单轮经验 | 每小时经验 | 提升');
topFert.forEach((r, i) => {
lines.push(
`${String(i + 1).padStart(2)} | ${r.name} | ${r.requiredLevel} | ${r.growTimeNormalFertStr} | ${r.expPerCycle} | ${r.farmExpPerHourNormalFert.toFixed(2)} | ${r.gainPercent.toFixed(2)}%`
);
});
lines.push('');
if (levelInfo) {
lines.push(`当前等级 Lv${levelInfo.level} 推荐`);
lines.push(`不施肥: ${levelInfo.bestNoFert.name}(seed=${levelInfo.bestNoFert.seedId}) -> ${levelInfo.bestNoFert.expPerHour.toFixed(2)} exp/h`);
lines.push(`普通肥: ${levelInfo.bestNormalFert.name}(seed=${levelInfo.bestNormalFert.seedId}) -> ${levelInfo.bestNormalFert.expPerHour.toFixed(2)} exp/h`);
lines.push('');
}
fs.writeFileSync(outPath, `${lines.join('\n')}\n`, 'utf8');
}
function analyzeExpYield(opts = {}) {
const lands = Math.max(1, Math.floor(toNum(opts.lands, 18)));
const level = opts.level == null ? null : Math.max(1, Math.floor(toNum(opts.level, 1)));
const top = Math.max(1, Math.floor(toNum(opts.top, 20)));
const input = opts.input || DEFAULT_INPUT;
const inputAbs = path.resolve(input);
const rawSeeds = readSeeds(inputAbs);
const seedPhaseReduceMap = loadSeedPhaseReduceMap();
const { rows, skipped, plantSecondsNoFert, plantSecondsNormalFert, missingPhaseReduceCount } = buildRows(rawSeeds, lands, seedPhaseReduceMap);
if (rows.length === 0) {
throw new Error('没有可计算的种子数据(请检查输入文件)');
}
const topNo = pickTop(rows, 'farmExpPerHourNoFert', top);
const topFert = pickTop(rows, 'farmExpPerHourNormalFert', top);
const bestByLevel = buildBestByLevel(rows);
let currentLevel = null;
if (level != null) {
currentLevel = bestByLevel.find(x => x.level === level) || null;
}
return {
generatedAt: new Date().toISOString(),
input: inputAbs,
config: {
lands,
plantSpeedPerSecNoFert: NO_FERT_PLANT_SPEED_PER_SEC,
plantSpeedPerSecNormalFert: NORMAL_FERT_PLANT_SPEED_PER_SEC,
plantSecondsNoFert,
plantSecondsNormalFert,
fertilizer: {
mode: 'minus_one_phase',
},
rule: {
expPerCycle: 'expHarvest',
},
},
stats: {
rawCount: rawSeeds.length,
calculatedCount: rows.length,
skippedCount: skipped,
missingPhaseReduceCount,
},
topNoFert: topNo.map(r => ({
seedId: r.seedId,
name: r.name,
requiredLevel: r.requiredLevel,
expPerHour: Number(r.farmExpPerHourNoFert.toFixed(4)),
})),
topNormalFert: topFert.map(r => ({
seedId: r.seedId,
name: r.name,
requiredLevel: r.requiredLevel,
expPerHour: Number(r.farmExpPerHourNormalFert.toFixed(4)),
gainPercent: Number(r.gainPercent.toFixed(4)),
})),
bestByLevel,
currentLevel,
rows,
};
}
function getPlantingRecommendation(level, lands, opts = {}) {
const safeLevel = Math.max(1, Math.floor(toNum(level, 1)));
const payload = analyzeExpYield({
input: opts.input || DEFAULT_INPUT,
lands: lands == null ? 18 : lands,
top: opts.top || 20,
level: safeLevel,
});
const availableRows = payload.rows.filter(r => r.requiredLevel <= safeLevel);
const bestNoFertRow = pickTop(availableRows, 'farmExpPerHourNoFert', 1)[0] || null;
const bestNormalFertRow = pickTop(availableRows, 'farmExpPerHourNormalFert', 1)[0] || null;
return {
level: safeLevel,
lands: payload.config.lands,
input: payload.input,
bestNoFert: bestNoFertRow ? {
seedId: bestNoFertRow.seedId,
name: bestNoFertRow.name,
requiredLevel: bestNoFertRow.requiredLevel,
expPerHour: Number(bestNoFertRow.farmExpPerHourNoFert.toFixed(4)),
} : null,
bestNormalFert: bestNormalFertRow ? {
seedId: bestNormalFertRow.seedId,
name: bestNormalFertRow.name,
requiredLevel: bestNormalFertRow.requiredLevel,
expPerHour: Number(bestNormalFertRow.farmExpPerHourNormalFert.toFixed(4)),
} : null,
candidatesNoFert: pickTop(availableRows, 'farmExpPerHourNoFert', opts.top || 20).map(r => ({
seedId: r.seedId,
name: r.name,
requiredLevel: r.requiredLevel,
expPerHour: Number(r.farmExpPerHourNoFert.toFixed(4)),
})),
candidatesNormalFert: pickTop(availableRows, 'farmExpPerHourNormalFert', opts.top || 20).map(r => ({
seedId: r.seedId,
name: r.name,
requiredLevel: r.requiredLevel,
expPerHour: Number(r.farmExpPerHourNormalFert.toFixed(4)),
gainPercent: Number(r.gainPercent.toFixed(4)),
})),
};
}
function main() {
const opts = parseArgs(process.argv.slice(2));
const payload = analyzeExpYield(opts);
const rows = payload.rows;
const topNo = pickTop(rows, 'farmExpPerHourNoFert', opts.top);
const topFert = pickTop(rows, 'farmExpPerHourNormalFert', opts.top);
const currentLevel = payload.currentLevel;
writeJson(path.resolve(opts.outJson), payload);
writeCsv(path.resolve(opts.outCsv), rows);
writeSummaryTxt(
path.resolve(opts.outTxt),
opts,
{
input: payload.input,
plantSecondsNoFert: payload.config.plantSecondsNoFert,
plantSecondsNormalFert: payload.config.plantSecondsNormalFert,
missingPhaseReduceCount: payload.stats.missingPhaseReduceCount,
},
topNo,
topFert,
currentLevel
);
console.log(`[收益率] 计算完成,共 ${rows.length} 条(跳过 ${payload.stats.skippedCount} 条)`);
console.log(`[收益率] JSON: ${path.resolve(opts.outJson)}`);
console.log(`[收益率] CSV : ${path.resolve(opts.outCsv)}`);
console.log(`[收益率] TXT : ${path.resolve(opts.outTxt)}`);
if (currentLevel) {
console.log(`[收益率] Lv${opts.level} 最优(不施肥): ${currentLevel.bestNoFert.name} ${currentLevel.bestNoFert.expPerHour} exp/h`);
console.log(`[收益率] Lv${opts.level} 最优(普通肥): ${currentLevel.bestNormalFert.name} ${currentLevel.bestNormalFert.expPerHour} exp/h`);
}
}
module.exports = {
analyzeExpYield,
getPlantingRecommendation,
DEFAULT_INPUT,
};
if (require.main === module) {
try {
main();
} catch (e) {
console.error(`[收益率] 失败: ${e.message}`);
process.exit(1);
}
}

View File

@@ -0,0 +1,127 @@
# 农作物列表 (共 123 种)
| ID | 名称 | 等级 | 季数 | 产量 | 生长(小时) | 阶段详情 |
|---|---|---|---|---|---|---|
| 1020001 | 草莓 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; |
| 1020002 | 白萝卜 | 1 | 1 | 5 | 0.0h | 种子:30;发芽:30;成熟:0; |
| 1020003 | 胡萝卜 | 1 | 1 | 10 | 0.0h | 种子:30;发芽:30;小叶子:30;大叶子:30;成熟:0; |
| 1020004 | 玉米 | 1 | 1 | 40 | 1.3h | 种子:960;发芽:960;小叶子:960;大叶子:960;开花:960;成熟:0; |
| 1020005 | 土豆 | 1 | 1 | 60 | 2.0h | 种子:1440;发芽:1440;小叶子:1440;大叶子:1440;初熟:1440;成熟:0; |
| 1020006 | 茄子 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; |
| 1020007 | 番茄 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; |
| 1020008 | 豌豆 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; |
| 1020009 | 辣椒 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; |
| 1020010 | 南瓜 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; |
| 1020011 | 苹果 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; |
| 1020013 | 葡萄 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; |
| 1020014 | 西瓜 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; |
| 1020015 | 香蕉 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; |
| 1020016 | 菠萝蜜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; |
| 1020018 | 桃子 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;开花:5760;小叶子:5760;大叶子:5760;成熟:0; |
| 1020019 | 橙子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; |
| 1020022 | 鳄梨 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020023 | 石榴 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;开花:5760;成熟:0; |
| 1020026 | 柚子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;开花:17280;成熟:0; |
| 1020027 | 菠萝 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; |
| 1020029 | 椰子 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; |
| 1020031 | 葫芦 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; |
| 1020033 | 火龙果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; |
| 1020034 | 樱桃 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; |
| 1020035 | 荔枝 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; |
| 1020036 | 箬竹 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; |
| 1020037 | 莲藕 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; |
| 1020038 | 木瓜 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; |
| 1020039 | 杨桃 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; |
| 1020041 | 红玫瑰 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; |
| 1020042 | 柠檬 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; |
| 1020043 | 无花果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; |
| 1020044 | 丝瓜 | 1 | 1 | 200 | 12.0h | 种子:8640;长枝:8640;开花:8640;小叶子:8640;大叶子:8640;结果:0; |
| 1020045 | 猕猴桃 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; |
| 1020047 | 甘蔗 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;幼苗:8640;分叶:8640;伸长:8640;成熟:0; |
| 1020048 | 杨梅 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; |
| 1020049 | 花生 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; |
| 1020050 | 蘑菇 | 1 | 2 | 200 | 4.0h | 种子:3600;发芽:3600;大叶子:3600;初熟:3600;成熟:0; |
| 1020051 | 红枣 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; |
| 1020052 | 金针菇 | 1 | 2 | 200 | 12.0h | 种子:7200;发菌:7200;出菇:7200;幼菇:10800;初熟:10800;成熟:0; |
| 1020053 | 桂圆 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020054 | 梨 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; |
| 1020055 | 枇杷 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; |
| 1020056 | 哈密瓜 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;开花:7200;成熟:0; |
| 1020057 | 芒果 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; |
| 1020058 | 榴莲 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020059 | 大白菜 | 1 | 1 | 20 | 0.1h | 种子:60;发芽:60;幼苗:60;成株:60;卷心:60;成熟:0; |
| 1020060 | 水稻 | 1 | 1 | 30 | 0.7h | 种子:480;幼苗:480;秧苗:480;幼穗:480;开花:480;成熟:0; |
| 1020061 | 小麦 | 1 | 1 | 40 | 1.0h | 种子:720;发芽:720;小叶子:720;大叶子:720;幼穗:720;成熟:0; |
| 1020062 | 四叶草 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; |
| 1020063 | 苦瓜 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020064 | 大葱 | 1 | 1 | 30 | 0.3h | 种子:300;发芽:300;小叶子:300;大叶子:300;成熟:0; |
| 1020065 | 大蒜 | 1 | 1 | 20 | 0.2h | 种子:120;发芽:120;幼苗:120;伸长:120;初熟:120;成熟:0; |
| 1020066 | 鲜姜 | 1 | 1 | 60 | 1.7h | 种子:1500;发芽:1500;小叶子:1500;大叶子:1500;成熟:0; |
| 1020067 | 香瓜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; |
| 1020068 | 冬瓜 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; |
| 1020070 | 黄豆 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;初熟:8640;成熟:0; |
| 1020071 | 小白菜 | 1 | 1 | 80 | 2.5h | 种子:2250;发芽:2250;小叶子:2250;大叶子:2250;成熟:0; |
| 1020072 | 榛子 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;开花:17280;小叶子:17280;大叶子:17280;成熟:0; |
| 1020073 | 菠菜 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; |
| 1020074 | 金桔 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; |
| 1020075 | 桑葚 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020076 | 山竹 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; |
| 1020077 | 蓝莓 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; |
| 1020078 | 杏子 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; |
| 1020079 | 番石榴 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; |
| 1020080 | 月柿 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; |
| 1020083 | 红毛丹 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;初熟:3600;成熟:0; |
| 1020084 | 芭蕉 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0; |
| 1020085 | 番荔枝 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0; |
| 1020086 | 橄榄 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020087 | 百香果 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;结果:0; |
| 1020088 | 灯笼果 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;结果:0; |
| 1020089 | 芦荟 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;开花:3600;成熟:0; |
| 1020090 | 薄荷 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020091 | 山楂 | 1 | 1 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:14400;开花:14400;初熟:14400;成熟:0; |
| 1020095 | 栗子 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; |
| 1020096 | 生菜 | 1 | 1 | 80 | 3.0h | 种子:2160;发芽:2160;小叶子:2160;大叶子:2160;初熟:2160;成熟:0; |
| 1020097 | 黄瓜 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;开花:8640;成熟:0; |
| 1020098 | 花菜 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;幼苗:8640;卷心:8640;初熟:8640;成熟:0; |
| 1020099 | 油菜 | 1 | 1 | 200 | 4.0h | 种子:3600;发芽:3600;小叶子:3600;大叶子:3600;成熟:0; |
| 1020100 | 竹笋 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;幼苗:2880;伸长:2880;初熟:2880;成熟:0; |
| 1020103 | 天香百合 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; |
| 1020104 | 非洲菊 | 1 | 1 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:2400;花蕾:2400;盛开:2400;成熟:0; |
| 1020105 | 小雏菊 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;初熟:2880;成熟:0; |
| 1020110 | 满天星 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0; |
| 1020116 | 曼陀罗华 | 1 | 2 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:3600;花蕾:3600;盛开:0; |
| 1020120 | 蒲公英 | 1 | 1 | 200 | 24.0h | 种子:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:17280;成熟:0; |
| 1020126 | 曼珠沙华 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; |
| 1020128 | 茉莉花 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; |
| 1020135 | 火绒草 | 1 | 1 | 200 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;花蕾:5760;盛开:0; |
| 1020141 | 花香根鸢尾 | 1 | 1 | 200 | 12.0h | 种子:8640;发芽:8640;小叶子:8640;大叶子:8640;花蕾:8640;盛开:0; |
| 1020142 | 虞美人 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0; |
| 1020143 | 含羞草 | 1 | 1 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:7200;花蕾:7200;盛开:7200;成熟:0; |
| 1020145 | 向日葵 | 1 | 1 | 200 | 4.0h | 种子:2400;发芽:2400;小叶子:2400;大叶子:2400;开花:2400;初熟:2400;成熟:0; |
| 1020147 | 牵牛花 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;花蕾:17280;盛开:0; |
| 1020161 | 秋菊(黄色) | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; |
| 1020162 | 秋菊(红色) | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶子:2880;大叶子:2880;花蕾:2880;盛开:0; |
| 1020201 | 天山雪莲 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;幼株:21600;成熟:0; |
| 1020202 | 金边灵芝 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;芝蕾:4800;幼芝:7200;初熟:7200;成熟:0; |
| 1020204 | 人参 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;初熟:7200;成熟:0; |
| 1020218 | 瓶子树 | 1 | 2 | 200 | 4.0h | 种子:2400;长枝:2400;小叶子:2400;大叶子:3600;初熟:3600;成树:0; |
| 1020220 | 猪笼草 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;初熟:10800;成熟:0; |
| 1020221 | 天堂鸟 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; |
| 1020222 | 豹皮花 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; |
| 1020225 | 宝华玉兰 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; |
| 1020226 | 依米花 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; |
| 1020227 | 大王花 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;幼蕾:4800;含苞:7200;初放:7200;盛开:0; |
| 1020228 | 人参果 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;开花:21600;成熟:0; |
| 1020229 | 何首乌 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; |
| 1020235 | 金花茶 | 1 | 2 | 200 | 8.0h | 种子:4800;发芽:4800;小叶子:4800;大叶子:7200;花蕾:7200;盛开:0; |
| 1020242 | 似血杜鹃 | 1 | 2 | 200 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;花蕾:21600;盛开:0; |
| 1020259 | 银莲花 | 1 | 1 | 200 | 4.0h | 种子:2880;发芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0; |
| 1020305 | 韭菜 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; |
| 1020306 | 芹菜 | 1 | 1 | 200 | 24.0h | 种子:17280;发芽:17280;小叶子:17280;大叶子:17280;初熟:17280;成熟:0; |
| 1020308 | 核桃 | 1 | 1 | 200 | 12.0h | 种子:10800;发芽:10800;小叶子:10800;大叶子:10800;成熟:0; |
| 1020396 | 迎春花 | 1 | 1 | 200 | 4.0h | 种子:2880;幼芽:2880;小叶:2880;大叶:2880;花蕾:2880;开花:0; |
| 1020413 | 李子 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;开花:10800;成熟:0; |
| 1020442 | 睡莲 | 1 | 2 | 200 | 12.0h | 种子:7200;发芽:7200;小叶子:7200;大叶子:10800;幼株:10800;成熟:0; |
| 1021542 | 新春红包 | 1 | 1 | 20 | 8.0h | 种子:5760;发芽:5760;小叶子:5760;大叶子:5760;初熟:5760;成熟:0; |
| 2020002 | 白萝卜 | 1 | 1 | 5 | 0.0h | 种子:1;发芽:1;成熟:0; |
| 2029998 | 哈哈南瓜 | 1 | 1 | 50 | 24.0h | 种子:14400;发芽:14400;小叶子:14400;大叶子:21600;初熟:21600;成熟:0; |

View File

@@ -0,0 +1,37 @@
const fs = require('fs');
const path = require('path');
const plantPath = path.join(__dirname, '../gameConfig/Plant.json');
try {
const data = fs.readFileSync(plantPath, 'utf8');
const plants = JSON.parse(data);
let content = `# 农作物列表 (共 ${plants.length} 种)\n\n`;
content += `| ID | 名称 | 等级 | 季数 | 产量 | 生长(小时) | 阶段详情 |\n`;
content += `|---|---|---|---|---|---|---|\n`;
plants.sort((a, b) => a.land_level_need - b.land_level_need || a.id - b.id);
plants.forEach(p => {
let totalTime = 0;
if (p.grow_phases) {
const parts = p.grow_phases.split(';');
parts.forEach(part => {
if (part) {
const [stage, time] = part.split(':');
if (time) totalTime += parseInt(time);
}
});
}
const hours = (totalTime / 3600).toFixed(1);
content += `| ${p.id} | ${p.name} | ${p.land_level_need} | ${p.seasons} | ${p.fruit ? p.fruit.count : '?'} | ${hours}h | ${p.grow_phases} |\n`;
});
fs.writeFileSync(path.join(__dirname, 'crop_list.md'), content);
console.log('列表已生成: server/tools/crop_list.md');
} catch (err) {
console.error('读取失败:', err);
}

File diff suppressed because it is too large Load Diff