📖 古龙精灵 API 参考手册
面向游戏脚本开发者的完整 API 参考文档。涵盖触控模拟、图像识别、OCR 文字识别、YOLO 目标检测、设备控制、网络请求等全部功能。
🎨 阅读主题(暗夜 / 暖色护眼)
本手册右上角提供「主题」按钮,可以按阅读环境切换配色。选择会自动保存在浏览器中,下次打开本页仍然生效;主题只影响本页面的显示样式,不会改动脚本代码、控制台输出或用户面板。
| 主题 | 主题内容(页面配色) | 适合场景 |
|---|---|---|
| 🌙 暗夜(默认) | 暗红/黑色背景、米金色标题与正文、深色代码块、青绿色 API 名称 | 夜间阅读、OLED 屏幕,与 PC 开发工具「黑色」主题一致 |
| ☀️ 暖色(护眼) | 米黄纸面背景、深棕正文、暖色代码高亮、低蓝光 | 白天或长时间阅读文档,与 PC 开发工具「暖色(护眼)」主题一致 |
💡 想恢复默认外观,点击右上角主题按钮重新选择「暗夜」即可。
📑 目录
- 1. 概览与快速入门
- 2. UI 控件参考(用户设置面板)
- 3. 脚本中获取用户设置参数
- 4. 触控方法(20个API)
- 5. 图像识别(13个API)
- 6. OCR文字识别(4个API)
- 7. YOLO目标检测(6个API)
- 8. 工具函数(57个API)
- 9. 设备方法(19个API)
- 10. Root与Shell(3个API)
- 11. 代理 IP(换 IP 防限号)(6个API)
- 12. Memory学习(进程内存读写)(3个API)
- 13. 无障碍方法(10个API)
- 14. 全分辨率方法(12个API)
- 15. 字符串方法(22个API)
- 16. 数组方法(7个API)
- 17. 数学方法与类型转换(32个API)
- 18. JSON方法(2个API)
- 19. 多线程(14个API)
- 20. 断点调试(开发工具)
- 21. 扩展方法与插件开发(9个API)
- 22. 完整脚本示例
🚀 概览与快速入门
什么是古龙精灵?
古龙精灵是一个面向游戏脚本开发者的全栈自动化平台。开发者使用 Lua 编写脚本,通过 PC 开发工具调试,最终打包为 APK 部署到 Android 设备上运行。平台提供丰富的 API 用于触控模拟、图像识别、OCR 文字识别、YOLO 目标检测等。
脚本基本结构
每个脚本是一个 Lua 文件,必须包含 main() 函数作为入口点。脚本引擎在加载文件后会自动调用 main()。
-- ============================================================ -- 古龙精灵脚本模板 -- ============================================================ -- 加载子模块 dofile("settings_fields.lua") -- 设置字段定义 dofile("server_api.lua") -- 服务器通信模块 -- 全局配置 GAMENAME = "我的游戏脚本" ACCOUNT_ID = "user_001" -- 主函数(脚本引擎自动调用) function main() log("脚本开始运行") -- 获取用户设置 local auto_fight = get_setting("auto_fight", true) -- 主循环 while true do if auto_fight then tap(500, 800) -- 点击攻击按钮 sleep(2000) -- 等待2秒 end sleep(1000) end end main()
开发流程
- 在 PC 开发工具中编写 Lua 脚本
- 使用工具 → 抓取窗口获取坐标、颜色、图片等数据
- 在开发工具中连接 Android 设备进行调试
- 通过开发者面板上传脚本包
- 打包 APK 分发给用户
运行环境与权限准备
触摸与按键注入依赖下面任意一条通道;两条都没有时,tap / swipe / swipeHuman / longTap / touchDown / touchUp 会返回 false,并在运行日志里写明原因(不会再静默失败,也不会用普通进程的 input 命令假装成功)。
| 通道 | 开启方式 | 能做什么 |
|---|---|---|
| 无障碍服务 | 系统设置 → 无障碍 → 已安装的服务 → 打开「古龙精灵」(推荐,无需 root;部分机型重启后需要重新确认) | tap / tapRandom / longTap / swipe / swipeHuman / touchDown 三件套;整条手势一次派发、最精确,会等手势真正走完才返回 |
| root 权限 | 设备已 root;脚本首次触摸时在超级用户弹窗点「允许」(持久 su 通道,只弹一次) | 最万能:多点触控、真正的「按住拖动」(touchDown/touchMove/touchUp)、物理按键、截图;su 通道长期驻留并已调低 oom_score_adj 抗后台回收 |
| 都没有 | — | 触摸类 API 返回 false,日志提示「请开启…无障碍服务或授予root权限」;不会再出现「脚本跑得好好的但屏幕没动」 |
·
点击操作失败!请开启…无障碍服务 → 无障碍服务未开启或已被系统关闭
·
无障碍点击/滑动未生效:…被系统取消(onCancelled) → 同一时刻已有别的手势在跑(上一次滑动还没走完就发了下一次),两次滑动之间加 sleep / random_sleep
·
没有配对的 touchDown → touchUp 之前漏了 touchDown,或中间被触摸状态重置清掉了
·
touchMoveEx … 没有可用的起点坐标,起点退化为目标点 → 漏了 touchDown(位移为 0,看起来「滑了但没动」)
· 轨迹要走曲线 / 每次都不一样 → 用 swipeHuman(swipe 是绝对直线,机器特征明显)
🎨 UI 控件参考(用户设置面板)
脚本可以在 SCRIPT_SETTINGS_FIELDS 中定义设置字段,这些字段会显示在用户控制面板的"脚本设置"选项卡中。所有控件定义使用 Lua table 格式。
控件结构层级
设置字段支持三层嵌套结构:fold(折叠面板) → tab_container(选项卡容器) → 控件字段;任意层都可以再用 hbox / row(横向)、vbox(纵向) 布局容器把多个控件排到一起。
SCRIPT_SETTINGS_FIELDS = { { key = "section_1", label = "战斗设置", type = "fold", -- 折叠面板 default = true, icon = "⚔️", fields = { { type = "tab_container", -- 选项卡容器 tabs = { { label = "基础", icon = "❤️", fields = { {key = "auto_fight", label = "自动打怪", type = "bool", default = true, icon = "⚔️"}, -- ... 更多控件 } }, } } }, }, }
1. ☑️ 布尔开关 (bool)
用于开启/关闭某个功能。必填属性:key, label, type, default。可选属性:icon, description。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | string | ✅ | 唯一标识,脚本中用此 key 读取值 |
| label | string | ✅ | 显示名称 |
| type | "bool" | ✅ | 控件类型 |
| default | boolean | ✅ | 默认值:true 或 false |
| icon | string | 图标(HTML实体编码) | |
| description | string | 提示文字 |
-- 基础开关(默认开启) -- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 {key = "auto_fight", label = "自动打怪", type = "bool", default = true, icon = "⚔️", description = "是否开启自动打怪功能"}, -- 带副标题开关 {key = "auto_pickup", label = "自动拾取", type = "bool", default = true, icon = "📦", description = "是否自动拾取掉落物品"}, -- 默认关闭开关 {key = "auto_sell", label = "自动出售装备", type = "bool", default = false, icon = "💰", description = "是否自动出售背包中的装备"}, -- 通知开关 {key = "enable_notify", label = "启用通知", type = "bool", default = true, icon = "🔔", description = "是否启用任务通知提醒"}, -- 调试模式开关 {key = "debug_mode", label = "调试模式", type = "bool", default = false, icon = "🐛", description = "开启后显示调试信息"},
2. 🔢 数字输入 (number)
用于输入数值。必填属性:key, label, type, default, min, max。可选:step, unit, icon, description。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | string | ✅ | 唯一标识 |
| label | string | ✅ | 显示名称 |
| type | "number" | ✅ | 控件类型 |
| default | number | ✅ | 默认值 |
| min | number | ✅ | 最小值 |
| max | number | ✅ | 最大值 |
| step | number | 步进值,默认1 | |
| unit | string | 单位(秒、%、分钟等) | |
| icon | string | 图标 | |
| description | string | 提示文字 |
-- 基础数字(带单位) -- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 {key = "fight_interval", label = "打怪间隔", type = "number", default = 5, min = 1, max = 60, step = 1, unit = "秒", icon = "⏱️"}, -- 百分比数字 {key = "hp_threshold", label = "回血阈值", type = "number", default = 30, min = 10, max = 90, step = 5, unit = "%", icon = "❤️"}, -- 时长数字(分钟) {key = "max_duration", label = "最大时长", type = "number", default = 120, min = 30, max = 1440, step = 10, unit = "分钟"}, -- 次数数字 {key = "max_attempts", label = "最大尝试次数", type = "number", default = 3, min = 1, max = 99, step = 1, unit = "次"}, -- 浮点数(带小数) {key = "move_speed", label = "移动速度", type = "number", default = 1.5, min = 0.5, max = 5.0, step = 0.1, unit = "倍"},
3. 📝 文本输入 (text)
用于输入文本。必填属性:key, label, type, default。可选:placeholder, password, multiline, icon, description。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | string | ✅ | 唯一标识 |
| label | string | ✅ | 显示名称 |
| type | "text" | ✅ | 控件类型 |
| default | string | ✅ | 默认值 |
| placeholder | string | 占位提示文字 | |
| password | boolean | 是否为密码输入框 | |
| multiline | boolean | 是否多行文本 | |
| icon | string | 图标 | |
| description | string | 提示文字 |
-- 基础文本(带占位符) -- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 {key = "target_monster", label = "目标怪物", type = "text", default = "银月贤者", icon = "🎯", placeholder = "输入怪物名称", description = "要自动攻击的怪物名称"}, -- 密码输入 {key = "account_password", label = "账号密码", type = "text", default = "", icon = "🔑", placeholder = "输入密码", password = true, description = "登录密码"}, -- 多行文本 {key = "custom_notice", label = "自定义公告", type = "text", default = "", icon = "📋", multiline = true, placeholder = "输入公告内容..."}, -- 过滤关键词 {key = "filter_keywords", label = "过滤关键词", type = "text", default = "", icon = "🔍", placeholder = "关键词用逗号分隔"},
4. 📋 下拉选择 (select)
用于从预设选项中选择。必填属性:key, label, type, default, options。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | string | ✅ | 唯一标识 |
| label | string | ✅ | 显示名称 |
| type | "select" | ✅ | 控件类型 |
| default | string | ✅ | 默认值(对应 option 的 value) |
| options | table | ✅ | 选项列表:{{value="x", label="显示名"}, ...} |
| icon | string | 图标 | |
| description | string | 提示文字 |
-- 基础下拉选择 -- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 {key = "difficulty", label = "难度选择", type = "select", default = "normal", icon = "🎮", options = {{value = "easy", label = "简单"},{value = "normal", label = "普通"},{value = "hard", label = "困难"},{value = "hell", label = "地狱"}}, description = "选择打怪难度等级"}, -- 地图选择 {key = "target_map", label = "目标地图", type = "select", default = "map1", icon = "🗺️", options = {{value = "map1", label = "新手村"},{value = "map2", label = "幽暗森林"},{value = "map3", label = "冰封雪域"},{value = "map4", label = "火焰山"}}}, -- 职业选择 {key = "class_type", label = "职业选择", type = "select", default = "warrior", options = {{value = "warrior", label = "战士"},{value = "mage", label = "法师"},{value = "archer", label = "弓箭手"},{value = "assassin", label = "刺客"}}}, -- 分辨率选择 {key = "resolution", label = "分辨率", type = "select", default = "720p", options = {{value = "720p", label = "720x1280"},{value = "1080p", label = "1080x1920"},{value = "2k", label = "1440x2560"}}},
key 中包含中文字符(例如 "difficulty_选择采集物品"),在入口文件中必须使用方括号语法 settings["key"] 来获取值,不能使用点号语法 settings.key。
-- ===== 📋 select(下拉选择)===== -- 读取用户选择的值,用于条件判断 local difficulty_选择采集物品 = settings["difficulty_选择采集物品"] or "wp_流寇" -- 【片段】下面这段「控件定义」属于 settings_fields.lua 的字段定义(不能单独运行/检查) -- 控件定义 {key = "difficulty_选择采集物品", label = "采集物品", type = "select", default = "normal", icon = "🎮", options = {{value = "wp_流寇", label = "流寇"},{value = "wp_煤矿", label = "煤矿"},{value = "wp_铁矿", label = "铁矿"},{value = "wp_伐木场", label = "伐木场"}}, description = "选择采集物品"}, -- 【可运行代码】下面这段可以直接运行/语法检查 -- ✅ 正确:使用方括号语法 local val = settings["difficulty_选择采集物品"] -- ❌ 错误:不能使用点号语法(key含中文时) -- local val = settings.difficulty_选择采集物品 -- 这会报错!
5. 📦 折叠面板 (fold)
用于将一组控件折叠/展开。必填属性:key, label, type, default, fields。fields 内部可以嵌套 tab_container,tab_container 的 tabs 里才是真正的控件。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | string | ✅ | 唯一标识 |
| label | string | ✅ | 显示名称 |
| type | "fold" | ✅ | 控件类型 |
| default | boolean | ✅ | 是否默认展开 |
| icon | string | 图标 | |
| description | string | 提示文字 | |
| fields | table | ✅ | 内部控件列表(通常含 tab_container) |
{
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查
key = "fight_settings",
label = "战斗设置",
type = "fold",
default = true,
icon = "⚔️",
description = "战斗相关设置(可折叠)",
fields = {
{
type = "tab_container",
tabs = {
{
label = "基础",
icon = "⚔️",
fields = {
{key = "auto_fight", label = "自动打怪", type = "bool", default = true, icon = "⚔️"},
{key = "fight_interval", label = "打怪间隔", type = "number", default = 5, min = 1, max = 60, step = 1, unit = "秒"},
}
},
{
label = "高级",
icon = "⚙️",
fields = {
{key = "debug_mode", label = "调试模式", type = "bool", default = false, icon = "🐛"},
}
},
}
}
},
},
5.1 ↔️ 横向布局容器 (hbox / row)
把多个控件放在同一行(例如两个开关并排、两个说明按钮并排)。子控件自动平分宽度,太窄时换行;行内可以是任意控件(bool / number / select / button / comment_popup 等),也可以再嵌套 hbox / vbox。子字段写在 fields 里(旧写法 children 也兼容);row 是 hbox 的别名,效果完全相同。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| type | "hbox" / "row" | ✅ | 横向布局容器(二者等价) |
| fields | table | ✅ | 同一行里的控件列表(也可写 children) |
| label | string | 说明文字(可选,vbox 会显示在顶部) |
-- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 { type = "hbox", fields = { {key = "auto_a", label = "三大陆", type = "bool", default = true, icon = "🌍"}, {key = "auto_b", label = "四大陆", type = "bool", default = false, icon = "🌎"}, } }, { type = "row", -- 等价写法:row = hbox fields = { {type = "comment_popup", label = "使用说明", title = "使用说明(必读)", content = "脚本会自动寻路打怪,请先手动走到怪物附近", style = "info"}, {type = "comment_popup", label = "更新日志", title = "更新日志", content = "本次更新内容", style = "success"}, } },
6. 📑 选项卡容器 (tab_container)
在折叠面板内部创建多个选项卡,每个选项卡包含一组控件。通常在 fold 的 fields 内部使用。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| type | "tab_container" | ✅ | 固定值 |
| tabs | table | ✅ | 选项卡列表,每个 tab 有 label, icon, fields |
7. 🎨 颜色选择 (color)
-- 颜色选择器 -- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 {key = "theme_color", label = "主题颜色", type = "color", default = "#FF5722", icon = "🎨", description = "选择界面主题颜色"}, -- 文字颜色 {key = "text_color", label = "文字颜色", type = "color", default = "#FFFFFF", icon = "✏️"},
📋 入口文件使用示例(完整)
以下示例展示如何在脚本入口文件 main() 中读取所有类型控件的值,并据其控制脚本逻辑。覆盖:bool、number、text、select、date、time、slider、file、multi_select、tag 等全部控件类型。这是实际项目中最核心的模式。
-- ============================================================ -- 第一步:加载设置字段定义文件 -- ============================================================ dofile("settings_fields.lua") -- ============================================================ -- 第二步:main() 入口函数 - 获取并读取所有控件值 -- ============================================================ function main() log("==================================================") log(" 古龙精灵 - 使用示例") -- ===== 获取用户设置 ===== local settings = get_user_settings() -- ===== ☑️ bool(布尔开关)===== -- 中文key必须用方括号 settings["key"] local auto_fight = false if settings["auto_fight_自动打怪"] ~= nil then auto_fight = settings["auto_fight_自动打怪"] end -- 英文key可用点号语法 local auto_pickup = settings.auto_pickup if auto_pickup == nil then auto_pickup = true end -- ===== 🔢 number(数字输入)===== -- 读取并在脚本中使用,单位由控件定义决定(秒、%、分钟等) local fight_interval = settings["fight_interval"] or 5 -- 控件单位:秒 local hp_threshold = settings.hp_threshold or 30 -- 控件单位:% local max_duration = settings.max_duration or 120 -- 控件单位:分钟 local move_speed = settings.move_speed or 1.5 -- 控件单位:倍(浮点数) log(" 📊 打怪间隔=" .. fight_interval .. "秒, 回血=" .. hp_threshold .. "%, 最长" .. max_duration .. "分钟, 移速x" .. move_speed) -- ===== 📝 text(文本输入)===== -- 读取用户输入的文本,常用于匹配目标名称、过滤关键词等 local target_monster = settings.target_monster or "银月贤者" local filter_keywords = settings.filter_keywords or "" log(" 📝 目标怪物=" .. target_monster .. ", 过滤词=" .. filter_keywords) -- ===== 📋 select(下拉选择)===== -- 读取用户选择的值,用于条件判断 local difficulty = settings.difficulty or "normal" local target_map = settings.target_map or "map1" if difficulty == "hell" then log(" ⚠️ 地狱难度已开启!") end -- ===== 📅 date(日期选择)===== local start_date = settings.start_date or "2025-01-01" -- 可用于判断当天是否在设定日期之后才执行 local y, m, d, h = get_current_time() local today = string.format("%d-%02d-%02d", y, m, d) if today > start_date then log(" 📅 日期已到达,开始执行...") end -- ===== ⏰ time(时间选择)===== local start_time = settings.start_time or "08:00" -- 解析时间字符串用于判断当前是否在指定时间之后 local start_hour = tonumber(start_time:sub(1, 2)) or 8 if h >= start_hour then log(" ⏰ 已过" .. start_time .. ",开始执行任务") end -- ===== 🎚️ slider(滑块)===== local volume = settings.volume or 80 -- 将滑块值(0-100)映射到实际逻辑(如点击速度比例) local click_speed = 1.0 + (volume / 100) * 2.0 -- 映射为 1.0~3.0 倍速 log(" 🎚️ 音量=" .. volume .. ", 点击倍速=" .. click_speed) -- ===== 📁 file(文件选择)===== local script_file = settings.script_file or "" if script_file ~= "" then log(" 📁 用户指定脚本文件: " .. script_file) -- 可以 dofile(script_file) 加载用户指定的脚本 end -- ===== ☑️ multi_select(多选列表)===== -- 多选列表返回的是 table(数组),包含所有选中的 value local target_maps = settings.target_maps or {} if #target_maps > 0 then log(" 🗺️ 多选地图: " .. table.concat(target_maps, ", ")) -- 遍历每个选中的地图执行逻辑 for i, map_name in ipairs(target_maps) do log(" 正在处理地图: " .. map_name) -- 跳转到对应地图、执行对应逻辑... end else log(" 🗺️ 未选择地图,使用默认地图") end -- ===== 🏷️ tag(分组标签)===== -- 分组标签返回的是 table(数组),包含所有选中的标签 value local item_filter = settings.item_filter or {} local filter_equip = false local filter_potion = false for i, tag in ipairs(item_filter) do if tag == "equip" then filter_equip = true end if tag == "potion" then filter_potion = true end end log(" 🏷️ 物品过滤: 装备=" .. tostring(filter_equip) .. ", 药水=" .. tostring(filter_potion)) -- ===== 🎨 color(颜色选择)===== local theme_color = settings.theme_color or "#FF5722" log(" 🎨 主题颜色: " .. theme_color) -- ===== 主循环:根据控件值控制脚本逻辑 ===== while true do if auto_fight then log("⚔️ 自动打怪开始!目标: " .. target_monster .. ", 难度: " .. difficulty) -- 调用对应的打怪子程序 else log("⚔️ 自动打怪已关闭") log(" [提示] 请在用户控制面板中开启自动打怪") end sleep(fight_interval * 1000) end end main()
8. 📅 其他控件
日期选择 (date)、时间选择 (time)、滑块 (slider)、文件选择 (file)、多选列表 (multi_select)、分组标签 (tag)
-- 日期选择 -- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 {key = "start_date", label = "开始日期", type = "date", default = "2025-01-01", icon = "📅"}, -- 时间选择 {key = "start_time", label = "开始时间", type = "time", default = "08:00", icon = "⏰"}, -- 滑块 {key = "volume", label = "音量调节", type = "slider", default = 80, min = 0, max = 100, step = 1, icon = "🔊"}, -- 文件选择 {key = "script_file", label = "脚本文件", type = "file", default = "", icon = "📄", placeholder = "选择脚本文件..."}, -- 多选列表 {key = "target_maps", label = "目标地图(多选)", type = "multi_select", default = {}, icon = "🗺️", options = {{value = "map1", label = "新手村"},{value = "map2", label = "幽暗森林"},{value = "map3", label = "冰封雪域"}}}, -- 分组标签 {key = "item_filter", label = "物品过滤", type = "tag", default = {}, icon = "🏷️", options = {{value = "equip", label = "装备"},{value = "potion", label = "药水"},{value = "material", label = "材料"}}},
9. 🔘 按钮控件 (button)
按钮控件点击后 执行脚本里的函数(func 指定的函数名)。平台会自动把动作派发给脚本执行 —— Lua 侧零代码(不需要自己写轮询/分发)。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| key | string | ✅ | 唯一标识(按钮不保存值,仅作占位) |
| label | string | ✅ | 按钮上显示的文字 |
| type | "button" | ✅ | 控件类型 |
| func | string | ✅ | 要执行的脚本函数名。必须与脚本里的函数名完全一致,且不能带 ();支持「模块表.函数名」写法(如 module_zx.一键日常) |
| style | string | 按钮样式:primary(默认)/ success / warning / danger | |
| icon | string | 图标(HTML实体编码) | |
| confirm | string | 二次确认文案(如 "确定要重置今日任务吗?")。危险操作建议同时设 style = "danger" | |
| description | string | 按钮下方的提示文字 |
-- 基础按钮:点击后执行脚本里的 判断_打开文件() -- 【片段】settings_fields.lua 字段定义片段:粘到字段列表里即可,不能单独运行/语法检查 {key = "btn_open_log", label = "查看日志文件", type = "button", func = "判断_打开文件", style = "primary", icon = "📄", description = "点击后执行脚本里的 判断_打开文件() 函数"}, -- 模块表里的函数:func 写「模块表.函数名」(支持) {key = "btn_daily", label = "一键日常", type = "button", func = "module_zx.一键日常", style = "success", icon = "🎮"}, -- 危险操作:加二次确认(confirm)+ 红色样式(danger) {key = "btn_reset_task", label = "重置今日任务", type = "button", func = "重置今日任务", style = "danger", icon = "⚠️", confirm = "确定要重置今日任务吗?", description = "需要用户确认后才会执行"},
-- 函数名与按钮 func 一致即可;中文函数名、模块函数都支持 function 判断_打开文件(params) local content = read_file_content("/sdcard/gulong/日志.txt") show_message_keep(content or "日志不存在", nil, nil, 1) end -- 模块表里的函数(对应 func = "module_zx.一键日常") module_zx = module_zx or {} function module_zx.一键日常(params) log("开始执行一键日常") end
执行机制(脚本侧无需配合):
- 点击按钮 → 面板把「函数名」发给平台;App 内面板点击走本机队列(不经过服务器、无网络延迟),用浏览器打开面板则走服务器队列;
- 脚本正在运行:平台在脚本的让出点(sleep / 各类 API 调用)自动派发执行(同一 Lua 线程,函数内部报错不会中断脚本);
- 脚本没在运行:平台自动启用「按钮直跑」宿主(独立 Lua 环境加载你的脚本,只加载函数、不调用 main()),首次点击约 1~2 秒准备,之后即时;
- 服务器会校验 token、「函数是否在 settings_fields.lua 的按钮里声明过」、账号剩余时间与触发频率;
start_script/stop_script/save_config/reset_config为内置动作,不会转发给脚本。
注意事项:
func必须与脚本里的函数名一致,不能带 ():写func = "判断_打开文件",不要写"判断_打开文件()";- 只有
settings_fields.lua里声明过的按钮函数才会被执行(白名单);改了 func 必须重新推送脚本; - 按钮函数建议写成自包含的(能自己读配置/文件,不依赖 main() 里才赋值的全局变量);参数固定从
params取(平台传入的参数表,通常为空表); confirm二次确认依赖 App 的 WebView 对话框(古龙精灵 / 古龙代码箱最新版已支持);用很老的 APK 时,点带 confirm 的按钮会没反应,装上最新 APK 即可。
🌐 公共设置(面板内置)
「公共设置」是用户面板「设置」Tab 顶部面板自带的一块配置(不属于脚本的 SCRIPT_SETTINGS_FIELDS,脚本不用声明、也读不到它的 values),目前包含两个字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| 开机启动脚本 | boolean | false(关) | 开启后:模拟器/设备开机时 APK 自动启动脚本,并自动关闭控制面板。等待期可点「立即启动」跳过剩余等待 |
| 开机后等待多少秒 | number | 20 | 开机后等待这么久再启动脚本,取值 0~600 秒(等模拟器稳定,建议 ≥20 秒) |
执行流程(脚本侧无需配合):
- 用户在面板点「💾 保存公共设置」→ 存到平台(按账号保存,与设备无关);
- App 内保存 / 加载时会把这份配置顺手缓存到本机,开机(BOOT_COMPLETED)时直接读缓存判断,不依赖网络;
- 开机流程:打开控制面板 → 等待 N 秒 → 启动脚本 → 关闭面板;App 起来后还会用平台的值校正一次(换设备 / 别处改了配置也能生效);
- 等待期屏幕提示:这 N 秒里控制面板上会盖一层动态倒计时(进度环 + 剩余秒数 + 呼吸光晕,每秒脉冲一次),下方还有「立即启动」按钮:不想等时可以点它跳过剩余等待,立即发送启动脚本指令并关闭面板;到点未点击则自动走同一流程。倒计时到点切成「正在启动脚本…」再自动关闭面板 —— 用户能明确看到「还在等、还有几秒」,不会以为卡死。想先看看效果,可在「设置 → 公共设置」点「🔍 预览倒计时效果」(预览时按钮显示为「结束预览」,点击只关闭预览,不会启动脚本);
- 开关关掉时只打开面板,不自动启动脚本(与旧版本行为一致)。
11. 💾 保存设置样式(样式1 / 样式2 预设)
用户面板「设置」Tab 底部的保存区域长什么样,由开发者按游戏配置:默认是样式1(一个「💾 保存设置」按钮,与老版本完全一样),也可以切成样式2(一行「设置N 下拉 + 💾 保存 + 📥 读取」,让用户把几套设置存起来来回切)。这项配置不需要写进 settings_fields.lua,也不需要重新推送脚本——用户在面板上刷新一次就能看到新样式。
| 对比项 | 样式1(默认) | 样式2(预设) |
|---|---|---|
| 用户看到的样子 | 一个「💾 保存设置」按钮 | 「设置N」下拉 + 「💾 保存」+「📥 读取」+ 一行提示 |
| 点一下做什么 | 把当前控件上的值写进服务端 | 保存:当前值存进选中的预设(本机)+ 立刻写服务端;读取:预设值回填到控件 + 立刻写服务端 |
| 多套设置 | 不支持(服务端只保留一套) | 支持 1~12 套,随时切换 |
| 预设内容存哪 | — | 用户手机的浏览器 localStorage;服务端仍只保存「当前生效的那套」 |
| 适合场景 | 设置项少、只有一种用法 | 设置项多、用户需要在几套配置之间来回切(不同副本 / 不同角色 / 不同任务) |
配置步骤(开发者面板)
- 打开开发者面板 →「🎮 游戏管理」;
- 找到目标游戏,点该游戏行的「💾 保存设置样式」;
- 在弹窗里选「样式1(默认)」或「样式2(预设)」;
- 选样式2 时填「🔢 预设个数」(1~12,默认 6),用户面板下拉会显示「设置1 ~ 设置N」;
- 点「💾 保存」(会写一条开发者日志
GAME_SAVE_SETTINGS_STYLE)。用户刷新 / 重新打开面板即生效,不需要重新推送脚本。
用户在样式2 下怎么用
- 💾 保存:把当前控件上的值存进下拉选中的那一格,并立刻写服务端(脚本下一次读配置就是这套值);
- 📥 读取:把那一格的值回填到控件,同样立刻写服务端,所以脚本会马上按这套值跑;
- 下拉记忆:面板会记住上次用过的格子(存在用户本机),下次打开面板下拉直接停在那格;
- ✏️ 改名:提示行右边的「✏️ 改名」可以给当前这格起个名字(最多 10 字,留空 = 恢复「设置N」);下拉里只显示名字(宽度全留给名字,手机上也不会被截断),提示行再给完整信息「设置3 · 打BOSS」,几套设置一眼就分得清;名字和预设一样只存在用户本机;
- 提示行:下拉下面那行显示这一格有没有内容(「设置3 · 打BOSS」已有内容,点「📥 读取」套用 / 还没有内容,点「💾 保存」存下当前设置);没起名就是「设置N」;
- 🌐 共享模板:开发者打开「允许用户提交模板」后,当前游戏的共享模板会直接并进这个「📱 本机预设」下拉(第二组,紫色 / 蓝灰)。共享模板只读:选中后「💾 保存」置灰、「✏️ 改名」失效,只有「📥 读取」可用(= 回填并立刻保存);提交入口仍是保存区下方的「📤 提交模板」;
- 开关 / 数字 / 文本 / 下拉 / 多选(multi_select) / 标签(tag) 都能存进预设,读取时一起还原;
- 预设存在用户本机:换手机 / 清缓存 / 换浏览器会丢,它不是平台上的云端配置。
脚本侧要不要改?
不用改。样式只影响面板上的按钮,脚本仍按老办法读「当前生效的那套设置」:
-- 面板上「💾 保存」/「📥 读取」都会立刻写服务端,所以这里读到的一定是用户当前选中的那套 local auto_fight = get_setting("auto_fight", false) local fight_interval = get_setting("fight_interval", 5) log("自动打怪: " .. tostring(auto_fight) .. ",间隔 " .. tostring(fight_interval) .. " 秒")
平台不会把「用户正在用第几套」写进 user_settings。脚本若确实需要知道,请让用户在 settings_fields.lua 里自己选一个下拉项,或由脚本用 set_setting() 维护一个字段。
save_settings_style 时,原来的预设个数会被保留,不会被重置;同理只传 allow_submit_template 不会改动样式。| 参数 | 类型 | 说明 |
|---|---|---|
| save_settings_style | number | POST 必填。1 = 老样式,2 = 预设样式;其它值一律按 1 处理 |
| save_settings_preset_count | number | 可选。预设个数 1~12,超出范围自动夹紧,非法值用默认 6 |
| allow_submit_template | bool | 可选。是否允许用户提交共享设置模板;缺省 false(关闭)。关闭后用户端只是没有「📤 提交模板」入口;已经提交的模板仍会出现在下拉里、可以继续读取套用,也不会删除已存数据 |
// 请求 { "save_settings_style": 2, "save_settings_preset_count": 8, "allow_submit_template": true } // 返回 { "success": true, "game": "梦幻逍遥", "game_key": "mhxy", "save_settings_style": 2, "save_settings_preset_count": 8, "allow_submit_template": true, "settings_template_limit": 12, "settings_templates": [ { "id": "a1b2c3d4", "name": "打BOSS", "field_count": 3, "submitter": "user001", "updated_at": "2026-10-21T15:00:00" } ], "message": "已将「梦幻逍遥」的「保存设置」设为样式2(8 个预设),已允许用户提交模板" }
① 配置是按游戏的:同一开发者下各游戏互不影响,没配过的游戏一律是样式1;
② 切回样式1 时面板只剩「💾 保存设置」按钮,用户本机已有的预设不会丢,再切回样式2 还能读出来;
③ 把预设个数改小(例如 8 → 4)时下拉只剩「设置1 ~ 设置4」,但用户上次用的那格仍被记住,改回 8 时会回到原来那格(预设内容也还在);
④ 预设存在用户手机里(localStorage 键名
userPanel_savePresets_<账号>_<开发者>),开发者看不到也改不了;⑤ 服务端始终只保留「当前生效的那套设置」,所以「多套并存」只在用户本机,脚本不需要也无法一次读多套;
⑥ 自定义名称也只存在用户本机(与预设同一个 localStorage),换手机 / 清缓存一起丢;名字只是给用户看的,不影响预设内容,也不会传给脚本。
共享设置模板(可选:用户提交,开发者管理)
同一个「保存设置样式」弹窗里勾选「📤 允许用户提交模板」并保存后,用户面板的保存区下方会多出「📤 提交模板」入口(样式1 / 样式2 都会显示)。用户可以把当前设置提交成模板(同名提交会覆盖自己的旧模板),开发者可以在本弹窗里查看提交人和模板内容概览、按需删除。模板按游戏存在服务端,不占用户本机的预设格子。
读取方式:样式2 下共享模板会直接并入「📱 本机预设」下拉(第二组「🌐 共享模板(只读,选中后点 📥 读取)」),不再单独放一个下拉 / 套用按钮。选中共享模板时它是只读的 ——「💾 保存」置灰、「✏️ 改名」失效,只有「📥 读取」可用:面板按 id 拉模板详情,把值回填到控件并立刻保存,脚本下一轮读配置就是这套值。下拉里 紫色 = 自己提交的(⭐),蓝灰 = 其他用户提交的(👤 · 打码账号)。样式1 没有预设下拉,共享模板只能提交、不能读取;要用共享模板请把「保存设置」改成样式2。
记忆:下拉会记住上次选中的那一项(本机格子或共享模板,存在同一个 localStorage 里),下次打开面板 / 重进「设置」页仍然选中它;共享模板被开发者删除后自动回落到本机记住的那一格。开发者关闭「允许用户提交模板」不影响已经提交的模板:它们仍然显示在下拉第二组、仍然可以读取套用(只是不再显示「📤 提交模板」入口)。
| 规则 | 说明 |
|---|---|
| 数量上限 | 每个游戏最多 12 个模板;满了再提交返回 409,需要开发者在弹窗里删掉旧模板 |
| 同名覆盖 | 同一账号在同一个游戏重复提交同名模板 = 更新那一条(不额外占名额),方便用户改完设置再提交 |
| 提交人展示 | 用户侧只显示打码账号(如 us***);开发者面板显示完整账号,方便清理。用户面板的模板下拉里,紫色 = 自己提交的(带「⭐ 我提交的」),蓝灰 = 其他用户提交的 |
| 关闭开关 | 只收起「📤 提交模板」入口(提交接口返回 403);列表 / 详情接口照常返回,用户仍能在下拉里选中并读取已提交的模板。模板只有在开发者手动删除或删除游戏时才会消失 |
| 删除游戏 | 删除游戏时同步清空该游戏的模板,避免重建同名游戏继承旧模板或继续占 12 个名额 |
token(带了就校验)、kaifazhe_id(面板注入)、game_key(当前游戏)。每条模板带 mine:true = 当前账号提交的(用户面板据此用紫色标注,其他人是蓝灰)。列表不受「允许提交模板」开关影响:游戏里已经提交过的模板始终返回,开关只体现在 allow_submit(用户端是否显示「📤 提交模板」)。{
"success": true,
"allow_submit": true,
"limit": 12,
"count": 1,
"game_key": "mhxy",
"game_name": "梦幻逍遥",
"templates": [
{ "id": "a1b2c3d4", "name": "打BOSS", "field_count": 3, "submitter": "us***", "mine": true, "created_at": "2026-10-21T15:00:00", "updated_at": "2026-10-21T15:00:00" }
]
}
{token?, kaifazhe_id?, game_key?, name, values}。name 最多 10 字(合并连续空白);values 只保留 JSON 基础类型 / 数组(多选、标签提交数组),服务端会限制字段数、单值长度和总大小。成功返回 updated(true = 覆盖了同账号同名模板)和模板元数据。values 回填控件,返回 {success, template, values}(管理端 / 二次开发也可以直接用)。不要求开发者开着「允许提交模板」:开关只控制提交入口,已提交的模板随时可以读取;定位不到游戏返回 404,模板已被删除返回 404。{"template_id": "a1b2c3d4"}。只删模板、不改开关;模板不存在返回 404。成功返回删除后的 settings_templates 列表。① 共享模板只存控件值(
values),不存字段结构;读取时按当前 settings_fields.lua 回填,字段被删除 / 改名后对应值会被忽略;② 模板内容由用户提交,开发者应在弹窗里检查后再保留;读取前请确认模板可信(读取会立刻覆盖当前设置并保存);提交人仅用于追溯,对其他用户只显示打码账号;
③ 12 个名额是每个游戏独立的,不同游戏互不影响。
⚙️ 脚本中获取用户设置参数
在脚本中,用户通过控制面板配置的设置可以通过多种方式获取。以下是完整的使用方法:
方法一:全局变量 user_settings
user_settings 是一个全局 table,包含用户在控制面板保存的所有设置。
while true do if user_settings.auto_fight then log("自动打怪已开启") -- 执行打怪逻辑 end sleep((user_settings.fight_interval or 5) * 1000) end
方法二:get_setting() 函数
安全地获取单个设置项的值,如果键不存在则返回默认值。
local auto_fight = get_setting("auto_fight", true) -- 获取布尔值,默认 true local fight_interval = get_setting("fight_interval", 5) -- 获取数字,默认 5 local target = get_setting("target_monster", "银月贤者") -- 获取文本 local difficulty = get_setting("difficulty", "normal") -- 获取下拉选择 if auto_fight then log("自动打怪已开启,间隔: " .. fight_interval .. "秒") end
方法三:get_live_setting() 函数(读取未保存的实时值)
读取用户在「用户面板 → 脚本设置」卡片里当前控件上的值,包括刚输入、还没点「保存设置」的数字 / 文本 / 开关 / 下拉选择。仅在安卓端(古龙精灵 / 古龙代码箱)由用户面板按钮触发的脚本函数中可用:面板点击按钮时会把所有控件的当前值随动作一起提交,平台在派发函数前保存为快照。
-- settings_fields.lua:声明输入框和按钮(按钮 func 指向下面的全局函数) {key = "boss_no", label = "Boss编号", type = "number", default = 1}, {key = "btn_run", label = "用当前输入执行", type = "button", func = "用实时值执行", style = "primary"} -- 用户改了数字但没点保存,点按钮后这里拿到的就是输入框里的最新值 function 用实时值执行(params) local boss_no = get_live_setting("boss_no", 1) local auto_fight = get_live_setting("auto_fight", false) local target = get_live_setting("target_monster", "银月贤者") log("本次使用实时值: " .. boss_no .. ",auto_fight=" .. tostring(auto_fight)) end
- 取值顺序:最近一次按钮点击的实时快照 → 已保存的
get_setting值 →default;快照里有这个键就一定优先用实时值。 - 不是持续同步:快照只在点击按钮时更新;用户输入后不点按钮,运行中的脚本不会自动看到新值。
- 不会自动保存:它只读,不会把值写回服务器;要保存请让用户点「保存设置」,或在脚本里调用
set_setting。 - 旧 APK 兼容:按钮函数也可以直接读
params.live_settings[key](面板会一并传入),不依赖新注入的全局函数;新 APK 推荐用get_live_setting。 - 设备端只有在用户面板按钮点击派发的脚本函数里才有实时快照;其它场景要读已保存的设置,请用
get_setting或server_api.get_user_settings()。
方法四:set_setting() / set_settings() 函数
在脚本中动态修改用户配置项的值,返回 true/false。新版服务器支持局部合并写入:只发改动的键、其它键保持原值,1 个请求就落库(响应带 merged=true);连到老版本服务器时自动回退成「先读 values → 合并 → 整体回写」,不会丢配置。要一次改多个键请用 set_settings(同样只有 1 个请求)。
🎯 一次改多个键想「同时跳变」就用 set_settings:客户端写设置复用同一条长连接(不再每个请求重新握手),面板还会把 250ms 内到达的多份快照合并成一次套用 —— 连着调用 set_setting 通常也会一起刷新,但要 100% 保证几个控件同一帧一起变,请用 set_settings({...})(1 个请求 = 1 份快照,几个开关一起亮/一起关)。
-- 写入成功后,用户面板里的对应配置会变成新值 if set_setting("auto_fight", false) then log("已关闭自动打怪") end set_setting("fight_interval", 10) -- 修改打怪间隔为10秒(自动合并,不会清掉其它设置) set_setting("target_monster", "boss") -- 切换目标怪物;table 会存成 JSON 字符串 -- 一次写多个键:只有 1 个请求,面板立刻看到全部新值(推荐在按钮函数里用) set_settings({ -- 下面这些也可以直接用 get_live_setting 读未保存的控件值 auto_fight = true, fight_interval = 10, boss_no = 3, }) -- 脚本还没登录(拿不到用户 token)时返回 false;账号取当前项目的登录账号 local ok = set_setting("boss_no", 5) if not ok then log("写入失败:请先确认已登录用户账号") end
get_live_setting 仅在用户面板按钮点击派发的脚本函数中可用(读取点击瞬间未保存的控件值),其它场景请用 get_setting 或 server_api.get_user_settings()。user_settings / get_setting 在「📦 代码箱调试」与已安装的「古龙精灵」「古龙代码箱」里均已注入——只读用户面板保存的 values,需要用户账号 + 用户 token(未登录时 get_setting 返回默认值、user_settings.xxx 为 nil),内部带 10 秒缓存,可以放心放在循环里读取。⚠️ user_settings 是「按需读取视图」:user_settings.auto_fight 这种按键读取可用,但 pairs(user_settings) / json_stringify(user_settings) 取不到内容,需要整表时请用 server_api.get_user_settings()。set_setting 读服务器上已有的 values → 合并本次修改 → 整体回写,需要用户账号 + 用户 token(账号口径 = 脚本用户,取当前项目的登录账号)。只有开发者账号登录时返回 false(开发者 token 与用户 token 是两套签名,服务器 /api/account/* 会拒绝);修改配置也可以直接在控制面板操作。方法五:从 settings 字典读取(节点模式)
在节点模式下,通过服务器 API 获取用户设置后使用。这是实际项目(梦幻逍遥脚本)中使用的方式:
-- 获取设置 local settings = get_user_settings() -- 读取配置项(中文 key 必须用方括号) local auto_fight_自动饱食度 = false if settings["auto_fight_自动饱食度"] ~= nil then auto_fight_自动饱食度 = settings["auto_fight_自动饱食度"] end auto_fight_自动打怪 = false auto_fight_自动打怪 = settings["auto_fight_自动打怪"] -- 控制主循环 if auto_fight_自动打怪 then log("⚔️ 自动打怪开始!") -- 执行打怪逻辑 else log("⚔️ 自动打怪已关闭") end -- ⚠️ 重要:包含中文的 key 必须使用方括号语法 settings["key"] -- 不能用 settings.key 点号语法
扁平化设置字段
当设置字段包含 fold 和 tab_container 嵌套结构时,使用 flatten_settings_fields() 函数提取所有字段的默认值。该函数为递归展开,支持任意层嵌套(例如 fold 里面再套 fold / tab_container):
function flatten_settings_fields(fields) local result = {} if type(fields) ~= "table" then return result end for _, field in ipairs(fields) do if type(field) == "table" then local ftype = field.type if ftype == "tab_container" then -- 递归展开所有 tab 中的字段 for _, tab in ipairs(field.tabs or {}) do for _, f in ipairs(flatten_settings_fields(tab.fields or {})) do table.insert(result, f) end end elseif ftype == "fold" or ftype == "hbox" or ftype == "row" or ftype == "vbox" or ftype == "group" then -- 容器:fold 里面可以再套 fold / tab_container,递归往下展开 for _, f in ipairs(flatten_settings_fields(field.fields or field.children or {})) do table.insert(result, f) end else table.insert(result, field) end end end return result end -- 获取所有默认设置 function get_default_settings() local settings = {} local flat = flatten_settings_fields(SCRIPT_SETTINGS_FIELDS) for _, field in ipairs(flat) do -- 跳过 divider / comment 等没有 key 的装饰性字段 if type(field) == "table" and field.key ~= nil then settings[field.key] = field.default end end return settings end
🖱️ 触控方法 20个API
· 无障碍服务(推荐,无需 root):系统设置 → 无障碍 → 已安装的服务 → 开启「古龙精灵」,支持点击 / 长按 / 滑动 / 曲线滑动
· root 权限(最万能):支持多点触控与真正的「按住拖动」(touchDown → touchMove → touchUp)、按键注入;持久 su 通道只弹一次授权
· 返回值就是真实结果:true = 手势被系统确认完成(root 通道为 su 确认执行);false = 通道不可用 / 手势被系统取消 / 超时 / 派发失败 —— 每条 false 都会在日志和浮窗里写明原因(详见「概览与快速入门 → 运行环境与权限准备」)
-- 点击坐标 tap(100, 100) -- 配合找图使用 local result = find_image("images/button.png") if result and result.success then tap(result.x, result.y) end
longTap(100, 100)
-- 方式1:矩形范围内随机点击 tapRandom(100, 200, 300, 400) -- 方式2:圆形半径内随机点击 tapRandom(500, 800, 50)
-- 从左滑到右 swipe(200, 200, 800, 200, 500) -- 向上滑动翻页 swipe(540, 800, 540, 200, 300)
-- ① 经典写法:曲线、时长、微抖全部自动随机 swipeHuman(200, 200, 800, 200) -- ② 经典写法:指定耗时 500ms + 弯曲强度 0.3(弯得更明显) swipeHuman(540, 800, 540, 200, 500, 0.3) -- ③ 间隔写法(对齐 xdvip.滑动_模拟真人SSF):采样间隔 0.1px + 波动 1,并拿到路径点 local path = swipeHuman(300, 1200, 700, 600, 0.1, 1) if path then log("走了 " .. path.n .. " 个点 / " .. path.duration .. "ms") local p = path[1] -- {x=, y=, t=},也可以写 p[1] / p[2] end -- ④ 循环滑动时配合随机等待,更像真人操作 for i = 1, 3 do swipeHuman(540, 800, 540, 200) random_sleep(300, 900) end -- ⑤ 在误触风险大的界面可传 curve=0(接近直线,但仍有微抖) swipeHuman(300, 900, 300, 300, 400, 0)
① 无障碍服务:touchDown 只「登记起点」、touchMove / touchMoveEx 只「登记目标点」,真正的触点动作在 touchUp 时按记录一次性派发(有登记移动 = 从起点按 time 滑到终点;没有 = 原地点击 50ms);
② root 权限:touchDown / touchMove / touchUp 真实按下 / 移动 / 弹起手指,可多指、可长按,此时 touchMove 之间的 sleep 才会产生实际位移;
③ 都没有:touchMove / touchUp 返回 false 并在日志写明原因(不会假装成功)。
⚠️ 必须按 fingerId 配对调用:touchDown → touchMove / touchMoveEx(可多次)→ touchUp;漏掉 touchUp 会残留按下状态(脚本结束 / 停止时会自动清理)。返回值只有 touchUp 代表本次手势的最终结果。
-- ① 推荐:按住拖动用 touchMoveEx(无障碍 / root 都支持),一行顶替几百次 touchMove touchDown(1, 100, 100) sleep(50) touchMoveEx(1, 300, 300, 800) -- 第4个参数 = 这段滑动的耗时 800ms touchUp(1) -- 无障碍:真正的滑动在这一步发出,它的返回值才是本次结果 -- ② 只在 root 通道下才需要逐步移动(每步 sleep 才有意义) touchDown(1, 297, 327) for i = 1, 20 do touchMove(1, 297, 327 - i * 15) sleep(16) -- 无障碍模式下 touchMove 只是登记目标点,这些 sleep 不会产生中间触点 end touchUp(1) -- ③ 常规滑动更推荐 swipeHuman(自动选通道 + 拟人曲线,一行搞定) swipeHuman(540, 800, 540, 200)
time = 本次滑动耗时(毫秒):无障碍模式在 touchUp 时按该时长发出滑动手势(需先 touchDown 再 touchUp);root 模式按约 16ms 一步插值移动(真正的按住拖动);两者都没有则返回 false 并提示开启无障碍或授权 root
⚠️ time ≤ 0 会按 300ms 处理;没有先 touchDown 时起点会退化成目标点(位移为 0,日志里会给出警告)
-- 从 (100,100) 按住后平滑滑到 (300,300),耗时 800 毫秒 touchDown(1, 100, 100) sleep(50) touchMoveEx(1, 300, 300, 800) -- 第4个参数 time=800 就是这段滑动的耗时 touchUp(1) -- 无障碍模式下真正的滑动在 touchUp 时按 time 发出
keyDown / keyUp:只有古龙输入法能真正只发 DOWN / UP;root / app 只能发完整按键(keyDown 会打日志说明,keyUp 返回 false,不再假成功)。
channel (string/table, 可选): auto(默认)/ ime / root / app / a11y(a11y 仅对 del/forward_del 有效);也可写 {channel="root"}
-- 按HOME键 keyPress("home") -- 按返回键 keyPress("back") -- 删除键:del = 退格(删光标前一个字符);forward_del = Delete(删光标后一个字符) keyPress("del") keyPress("forward_del") -- 强制走 root 通道(游戏只认硬件删除键时) keyPress("del", "root") keyPress("del", {channel = "root"}) -- 账号/密码框建议直接按文本删除,不依赖硬件 DEL imeLib.lock() imeLib.deleteChar() imeLib.clearText() -- 按下并弹起(只有古龙输入法能真正只发 DOWN/UP) keyDown("home") sleep(100) keyUp("home")
-- 常规输入(聊天框 / 账号 / 密码框) tap(300, 800) -- 先点击输入框唤起输入法 sleep(800) inputText("hello123") -- 中文/空格请用输入法通道 imeLib.setText("你好 world")
-- 推荐:锁定古龙输入法(不弹键盘,支持中文) setStopCallBack(function(error) imeLib.unlock() end) imeLib.lock() imeLib.setText("公会副本马上开始") imeLib.finishInput() -- 回车/发送 imeLib.unlock() -- 手动解锁(脚本停止时也会自动解锁) -- 账号 + 密码登录框 tap(acc_x, acc_y) imeLib.setText("player123") tap(pwd_x, pwd_y) imeLib.setText("p@ss w0rd") -- 含空格/符号也可输入 -- 删除一个字符 / 清空整个输入框 imeLib.deleteChar() imeLib.clearText() -- 一次清空,适合输错重输
-- 推荐:把脚本的收尾清理都写进停止回调,手动停止 / 脚本跑完 / 报错退出都会执行 setStopCallBack(function(error) imeLib.unlock() -- 解锁古龙输入法,恢复系统输入法 close_message_keep() -- 关闭 show_message_keep 驻留消息窗 execRoot("settings put global policy_control null") -- 恢复状态栏显示 end) -- 最简用法:只做一件事 setStopCallBack(function(error) imeLib.unlock() end)
function onTouchEvent(x, y) log("触摸坐标: " .. x .. ", " .. y) end setOnTouchListener(onTouchEvent) sleep(1000000)
🔍 图像识别 13个API
-- 1) 截图到内存(用于找图 / 切图) local img = screenshot() local crop = image_crop(img, 100, 200, 300, 400) -- 2) 截图并保存到文件(设备端与 PC 本地都支持) screenshot("output/screen.png") -- 3) 截图后直接找图(模板用路径方式) if find_image("images/target.png") then log("找到目标") end
x1, y1, x2, y2 (number, 可选): 搜索区域(脚本坐标),省略或 0 = 全屏
threshold (number, 可选): 相似度 0.0~1.0,默认 0.9(模板中「颜色达标像素」占比达到该值才算命中)
tol (number, 可选): 偏色,每通道允许的颜色偏差 0~255,默认 10(0 = 要求逐像素完全一致)
-- 1) 路径方式(图片放在项目 images/ 下,会随项目下发到设备) local result = find_image("images/button.png") if result and result.success then tap(result.x, result.y) log(string.format("相似度: %.2f", result.confidence)) end -- 2) __base64__ 内嵌数据(抓取窗口可直接生成,不依赖设备上有图片文件) local TPL = "__base64__:iVBORw0KGgoAAAANSUhEUg..." local r = find_image(TPL, 0, 0, 1080, 1920, 0.9, 10) if r and r.success then tap(r.center_x, r.center_y) end -- 3) 只在小区域内找 + 放宽相似度 / 偏色(找得慢就缩小区域,误匹配多就调高相似度) local r2 = find_image("images/icon.png", 100, 200, 500, 800, 0.8, 20) -- 4) 兼容写法:旧签名(模板, 阈值)仍然可用 local r3 = find_image("images/target.png", 0.8)
timeout (number, 可选): 超时时间(秒),默认 10
threshold (number, 可选): 相似度,默认 0.9
x1, y1, x2, y2 (number, 可选): 搜索区域(脚本坐标)
tol (number, 可选): 偏色(每通道容差),默认 10
-- 等待图片出现(最多等10秒) local result = wait_image("images/loading_done.png") if result then tap(result.x, result.y) end -- 等 30 秒 + 限定区域 + 相似度 0.85 local r = wait_image("images/login_success.png", 30, 0.85, 0, 0, 1080, 600) if not r then log("登录超时!") end
threshold (number, 可选): 相似度,默认 0.9
x1, y1, x2, y2 (number, 可选): 搜索区域(脚本坐标)
tol (number, 可选): 偏色(每通道容差),默认 10
if exists("images/button.png") then log("按钮存在") tap(100, 200) end -- 限定区域 + 放宽偏色(适合图标在不同机器上有色差的场景) if exists("images/success.png", 0.85, 0, 0, 1080, 600, 20) then log("成功!") end
-- 把抓取窗口生成的 __base64__ 数据存进脚本,运行时解码成图片数据 local TPL = table.concat({ "__base64__:iVBORw0KGgoAAAANSUhEUg...", "...", }) local img = base64_decode(TPL) local r = find_image(img, 0, 0, 1080, 1920, 0.9, 10) if r and r.success then tap(r[1], r[2]) end -- 说明:直接把 "__base64__..." 字符串交给 find_image 也可以,效果一样
-- 获取坐标颜色并解析RGB local color = get_color(500, 300) local r, g, b = tonumber("0x"..color:sub(1,2)), tonumber("0x"..color:sub(3,4)), tonumber("0x"..color:sub(5,6)) log(r..","..g..","..b) -- 判断颜色 if r > 200 then log("该点偏红") end
-- 查找所有红色像素 local points = find_color({255, 0, 0}, 20) log("找到 " .. #points .. " 个红色像素点")
local result = findMultiColor(0, 0, 1080, 1920, "ff0000", "10|0|00ff00|20|0|0000ff", 0, 0.9) if result then tap(result[1], result[2]) end
local MultiColorTargets = { {"845635", "-9|8|d7c189|-77|-3|7a4829"}, {"efe3d3", "36|-18|e3c288|-10|6|efe1d3"}, } local result = findMultiColorTargets(0, 0, 1080, 1920, MultiColorTargets, 0, 0.9) if result then tap(result[1], result[2]) end
local MultiColorStr = { {"系统", "e7c548", "2|15|f2a91a|-25|-3|dac04f"}, {"返回", "ff0000", "10|0|00ff00|20|0|0000ff"}, } local result = findMultiColorStr(0, 0, 1080, 1920, MultiColorStr, 0, 0.9) if result then tap(result[1], result[2]) end
local MultiColorStr = { {"天兵", "e7c548", "2|15|f2a91a|-25|-3|dac04f"}, {"亢金龙", "efe3d3", "36|-18|e3c288|-10|6|efe1d3"}, } local r = getMultiColorStr(0, 0, 1080, 1920, MultiColorStr, 0, 0.9) if r and r.success then log("当前界面元素: " .. r.text) end
-- 比较多点颜色 local ret = cmpColorEx("ff0000", "10|0|00ff00|20|0|0000ff", 0, 0.9) if ret ~= -1 then log("颜色匹配成功") end -- 查找所有匹配位置 local results = findMultiColorAll(0, 0, 1080, 1920, "ff0000", "10|0|00ff00|20|0|0000ff") for i, pos in ipairs(results) do tap(pos[1], pos[2]) end
local number = getColorNum(10, 50, 600, 600, "e91e1e", 0.8) print(number) -- 判断按钮状态:区域内红色像素多 → 按钮点亮 local redNum = getColorNum(100, 200, 300, 250, "e91e1e", 0.9) if redNum > 200 then tap(200, 225) end -- 多颜色 + 偏色 local num = getColorNum(0, 0, 500, 300, "ff0000|00ff00-202020", 0.85)
📝 OCR文字识别 4个API
使用本地ONNX模型进行文字识别,无需联网。
ocr_recognize / ocr_find_text / ocr_wait_text /
ocr_recognize_detail,参数、返回值和 region 含义都不同,不要混用。
local text = ocr_recognize_local("ch", { 987, 332, 1121, 368 }) if text.success then log("识别结果: " .. text.text) if text.text == "开始挑战" then tap(1050, 350) end end
未找到时 {found=false, text=""};出错时(截屏失败/引擎未初始化/推理异常){success=false, error="原因"};text 始终存在,可安全做字符串拼接
local pos = ocr_find_text_local("开始", nil, {987, 332, 1121, 368}) if pos and pos.found then -- 点击文字中心 tap(pos.x + pos.w / 2, pos.y + pos.h / 2) end
⚠️ 没有 count / text 字段:数量用
#detail.results,全部文字要自己拼接;写 detail.count 会报 attempt to concatenate a nil value⚠️ 失败时
{success=false, error="原因", results={}}(截屏失败 / OCR引擎未初始化 / 推理异常),先看 error 再排查⚠️ x/y 已是脚本设计分辨率坐标,可直接 tap;「古龙精灵」PC 调试引擎未注册本函数(PC 用
ocr_recognize_detail,返回文字块数组、字段为 width/height)local detail = ocr_recognize_detail_local("ch", {800, 300, 1200, 500}) if type(detail) == "table" and detail.success then -- 没有 detail.count / detail.text:数量用 #detail.results,文字自己拼 log("识别到 " .. #(detail.results or {}) .. " 个文字块") for i, item in ipairs(detail.results or {}) do if string.find(tostring(item.text or ""), "开始挑战", 1, true) then -- 中心点取整:小数进 %d 会报 number has no integer representation tap(math.floor(item.x + item.w / 2), math.floor(item.y + item.h / 2)) break end end else log("识别失败: " .. tostring(detail and detail.error or detail)) end
local result = ocr_find_text_all_local("确认", nil, {100, 200, 600, 400}) if result.found then -- 从下往上点击所有匹配项 for i = result.count, 1, -1 do local item = result.results[i] tap(item.x + item.w / 2, item.y + item.h / 2) sleep(500) end end
🤖 YOLO目标检测 6个API
local result = yolo_detect() if result.success then log("检测到 " .. result.count .. " 个物体") for i, det in ipairs(result.detections) do log(string.format("%s: %.2f, (%d,%d)", det.class_name, det.confidence, det.center_x, det.center_y)) end -- 点击第一个检测到的物体 if result.count > 0 then tap(result.detections[1].center_x, result.detections[1].center_y) end end
📌 设备端未提供本 API(调用会报
attempt to call a nil value),请改用 yolo_load_model + yolo_detect。-- 加载自定义模型 local result = yolo_load_custom_model("resources/models/yolov11_npc.param") if result.success then log("类别: " .. table.concat(result.class_names, ", ")) -- 执行检测 local det = yolo_detect(0.35) end
📌 运行时支持:
yolo_detect_and_draw、yolo_load_model 设备端可用;yolo_detect_file、yolo_get_available_models 设备端未提供(调用会报 attempt to call a nil value)。-- 从文件检测 local result = yolo_detect_file("screenshot.png") -- 检测并绘制 local result = yolo_detect_and_draw() -- 加载内置模型 yolo_load_model("yolo11n") -- Nano最快 yolo_load_model("yolo11s") -- Small -- 查看可用模型 local models = yolo_get_available_models() for i, m in ipairs(models) do log(m.name .. ": " .. (m.exists and "✅" or "⬇️")) end
🔧 工具函数 57个API
sleep(2000) -- 等待2秒 sleep(500) -- 等待0.5秒
random_sleep(1000, 3000) -- 随机等待1~3秒 tap(500, 300) random_sleep(300, 800) -- 模拟人类点击间隔 tap(500, 400)
log("脚本开始执行") log(string.format("坐标: (%d, %d)", x, y))
user_settings / get_setting 在「📦 代码箱调试」与已安装的「古龙精灵」「古龙代码箱」里均已注入(只读、需用户账号 + 用户 token,未登录时 get_setting 返回默认值、user_settings.xxx 为 nil,内部 10 秒缓存;⚠️ user_settings 不支持 pairs() / json_stringify(),需要整表请用 server_api.get_user_settings())。set_setting / set_settings 返回 boolean:服务器支持局部合并时只发改动的键(1 个请求),老服务器自动回退成「读现有 values → 合并 → 整体回写」,不丢配置。账号口径 = 脚本用户:取当前项目登录账号;脚本还没登录(没有用户 token)时返回 false。获取/设置用户在控制面板中配置的参数;未保存的实时控件值请用
get_live_setting(详见上方"脚本参数获取"章节)-- 全局变量 if user_settings.auto_fight then log("自动打怪已开启") end -- 安全获取 local val = get_setting("key", defaultValue) -- 动态修改(返回 boolean;table 等复杂值会存成 JSON 字符串) local ok = set_setting("key", newValue)
params.live_settings,平台派发函数前保存为快照。查找顺序:最近一次按钮点击的实时快照 → 已保存的
get_setting 值 → default。⚠️ 只在点击按钮时更新,不是持续同步;不会自动保存。按钮函数也可以直接读 params.live_settings[key](兼容旧 APK)。-- 在面板按钮触发的全局函数内使用(params 由平台传入) local boss_no = get_live_setting("boss_no", 1) local auto_fight = get_live_setting("auto_fight", false) local target = get_live_setting("target_monster", "银月贤者") -- 兼容旧 APK:直接读动作参数里的快照 local live = (params and params.live_settings) or {} local boss_no_old = live.boss_no or get_setting("boss_no", 1)
-- 1) 面板按钮声明(settings_fields.lua) -- 【片段】第 1 步的按钮声明属于 settings_fields.lua(不能单独运行/检查) {key = "btn_open_log", label = "查看日志", type = "button", func = "判断_打开文件", style = "primary", confirm = "确定查看?"} -- 【可运行代码】下面这段可以直接运行/语法检查 -- 2) 脚本只写业务函数即可(平台会自动派发,不需要轮询) function 判断_打开文件(params) show_message("已执行一键日常") end -- 3)【可选】自己掌控执行时机时才需要下面这段 for _, act in ipairs(poll_user_actions() or {}) do local f = _G[act.func] if type(f) == "function" then pcall(f, act.params) end end
local resp = http_get("http://api.example.com/data") local data = json_parse(resp) local resp = http_post("http://api.example.com/login", nil, {username = "admin", password = "123456"})
open_url("http://www.baidu.com")
-- 以古龙精灵为例:相对路径 = App 内置 scripts 目录 local ok = write_file("result.txt", "执行完成") log("写入结果: " .. tostring(ok)) -- 写入JSON配置 local data = {time = os.time(), status = "ok"} write_file("data.json", json_stringify(data)) -- ===== 绝对路径示例(雷电模拟器)===== -- /sdcard 等价于 /storage/emulated/0 local ok2 = write_file("/sdcard/Download/gulong/data.txt", "Hello World") log(read_file("/storage/emulated/0/Download/gulong/data.txt")) -- 雷电共享目录(Windows 对应:文档\LDPlayer\Pictures) write_file("/mnt/shared/Pictures/gulong_test.txt", "Hello LDPlayer")
⚠️ 返回值是
boolean(true = 删除成功,false = 文件/目录不存在或删除失败):把它拼进字符串必须用 tostring 包一层,否则报 attempt to concatenate string and boolean;另外「古龙代码箱」的 log 只接收第一个参数(多写的参数会被忽略),所以统一「先 tostring 拼成一个字符串再 log」在各种情况下都安全。-- 相对路径(「古龙精灵」= App 内置 scripts 目录 / 「古龙代码箱」= 当前工程目录) local del_ok = delete_file("data.txt") -- delete_file 返回 boolean(true/false),必须 tostring;否则 string .. boolean 会报 -- "attempt to concatenate string and boolean"。也可写成: -- log("结果: " .. (del_ok and "删除成功" or "删除失败/文件不存在")) log("结果: " .. tostring(del_ok)) -- 用 if 判断更直观 if delete_file("old_log.txt") then log("旧日志已删除") else log("旧日志不存在或删除失败") end -- ===== 绝对路径示例(雷电模拟器)===== -- /sdcard 等价于 /storage/emulated/0;无权限时设备已 root 会自动用 su(rm -rf)回退 local del_abs = delete_file("/sdcard/Download/gulong/data.txt") log("删除结果: " .. tostring(del_abs)) -- 删除整个目录(含子文件,递归删除) log("目录删除: " .. tostring(delete_file("/sdcard/Download/gulong/old_logs"))) -- 雷电共享目录(Windows 对应:文档\LDPlayer\Pictures) delete_file("/mnt/shared/Pictures/gulong_test.txt")
local result = call_python("scripts/my_module.py", "process_data", "input.txt")
ACCOUNT_ID / _G_TOKEN / SERVER_URL(脚本 login() 后就有值)→ ② 项目目录下的 account_info.txt(启动脚本时 App 会把当前登录账号自动写进去,首行账号、次行 token)→ ③ 已登录的开发者账号(只能展示,写用户设置会被拒绝)。账号ID / 服务器地址都取不到时返回 nil。⚠️ 与「设备搜索 → 连接用户」里填的账号无关(那个账号只用于「连接用户截图」)。返回字段:
account_id / token / server_url / logged_in。local account = get_account_info() if account then log("账号: " .. account.account_id .. " @ " .. account.server_url) else log("未登录 / 没有账号上下文") end
nil)。字段:
device_id / device_name / brand / model / manufacturer / os_version / sdk_int / platform(设备端恒为 android)/ resolution / screen_width / screen_height / density。local info = get_device_info() log("设备: " .. info.device_name .. " (" .. info.brand .. " " .. info.model .. ")") log("分辨率: " .. info.resolution .. " 密度: " .. info.density) log("系统: Android " .. info.os_version .. " (SDK " .. info.sdk_int .. "), 运行环境: " .. info.platform)
📌 运行环境:PC 本机运行 /「古龙精灵」/「古龙代码箱」均已注入(需要脚本用用户账号登录,开发者 token 会被拒绝)。
📌 限制:单条最多 100 个字符(超出自动截断,按 Unicode 字符计);客户端 2 秒内只允许发送 1 条;服务端再按「每账号每分钟 30 条 / 每开发者每分钟 300 条」限频。
📌 游戏名省略时自动取脚本全局
GAMENAME;匹配不到「游戏管理」里的游戏时归入「其他」。另有 report_message_nowait(...) 在后台线程发送,不阻塞脚本。content (string):回传内容(≤100 字符)game (string, 可选):游戏名,默认取全局 GAMENAME{success=true, id, total} 或 {success=false, error}-- 推荐写法:显式传 GAMENAME(开发者面板按游戏分类) local r = server_api.report_message("开始执行:第 " .. round .. " 轮", GAMENAME) if not r.success then log("回传失败: " .. tostring(r.error)) end -- 也可以直接调用全局函数(等价写法) report_message("任务完成,用时 " .. seconds .. " 秒") -- 高频循环里用 nowait:后台发送,不阻塞脚本(2 秒内重复调用会被节流) report_message_nowait("血量 " .. hp .. "/" .. hp_max, GAMENAME)
Bug截图_脚本_账号_时间.png。📌 运行环境:「古龙精灵」/「古龙代码箱」已注入;PC 本机运行暂不支持。
📌 与悬浮窗「📷 Bug截图」按钮走完全相同的上传链路:先撤掉悬浮提示再截干净图,PNG 超过 8MB 自动降级 JPEG。
📌 限制:每账号每天最多 60 张(脚本额度与按钮的 20 张独立计数);客户端 2 秒内最多 1 张,服务端也会再限一次;上一张还在处理时立即返回
success=false。另有 bug_screenshot_nowait() 在后台线程截图上传、不阻塞脚本(返回 true=已排队)。{success=true, filename, used, limit, remaining, source='script'} 或 {success=false, error, retry_after?}-- 阻塞版:等截图 + 上传完成,返回本次额度 local r = server_api.bug_screenshot() if r.success then log("截图已发送: " .. tostring(r.filename) .. ",今日 " .. tostring(r.used) .. "/" .. tostring(r.limit)) else log("截图失败: " .. tostring(r.error)) end -- 后台版:不阻塞脚本,返回 true=已排队(false=未登录/2 秒节流/上一张未完成) if bug_screenshot_nowait() then log("截图任务已提交") end
play_sound 会打断上一个。📌 运行环境:「古龙精灵」与「古龙代码箱」均已实现;音量走
STREAM_MUSIC(与 get_volume / set_volume 默认音频流一致),系统媒体音量为 0 时同样听不到。📌 音频文件路径:「古龙精灵」相对路径 = App 内置 scripts 目录;「古龙代码箱」相对路径 = 当前工程目录;也支持绝对路径与
assets: 前缀(App 内置 assets)。· 数字 = ToneGenerator 音调编号(0~98)
· 字符串 = 内置提示音名:beep(tick、default)/ beep2(double)/ ok(ack、success、confirm)/ error(nack、fail)/ warn(warning)/ prompt(notice、ding)/ ring(ringtone)/ dial / busy / congestion / waiting(callwait)/ intercept / star(dtmfstar)/ hash(pound、dtmfhash)/ dtmf0~dtmf9;不认识的字符串按音频文件路径处理
volume (number, 可选): 音量百分比 0~100,默认 80(<= 0 不播放)
duration (number, 可选): 内置提示音时长(毫秒),默认 400,自动夹到 30~5000;<= 0 表示按该提示音的默认长度播一遍(对音频文件无效)
loop (boolean, 可选): 仅对音频文件有效,是否循环播放,默认 false(播完自动释放)
-- 播放内置提示音(默认音量 80、时长 400ms) play_sound("ok") -- 自定义音量 + 时长(约 1 秒) play_sound("error", 100, 1000) -- 播放本地音频文件(相对路径:古龙精灵 = App 内置 scripts 目录,古龙代码箱 = 当前工程目录) play_sound("alert.mp3") -- 绝对路径 / App 内置 assets 里的音频 play_sound("/sdcard/Music/bgm.mp3", 60) play_sound("assets:alert.mp3", 80) -- 循环播放背景音,10 秒后 stop_sound() 停止 play_sound("/sdcard/Music/bgm.mp3", 50, 0, true) sleep(10000) stop_sound()
-- 循环播放 10 秒后停止 play_sound("/sdcard/Music/bgm.mp3", 50, 0, true) sleep(10000) stop_sound() -- 脚本结束 / 停止前确保不再有声音(脚本停止时 App 也会自动释放) stop_sound()
更多工具函数(简要列表)
| 函数名 | 说明 | 示例 |
|---|---|---|
| get_clipboard | 获取剪贴板内容 | local text = get_clipboard() |
| set_clipboard | 设置剪贴板内容 | set_clipboard("文本") |
| get_battery_level | 获取电池电量百分比(取不到返回 -1) | local b = get_battery_level() |
| get_network_type | 获取网络类型 | local n = get_network_type() |
| is_screen_on | 检查屏幕是否亮起 | if not is_screen_on() then ... |
| wake_screen | 唤醒设备屏幕 | wake_screen() |
| get_volume / set_volume | 获取/设置音量(默认媒体音量,可传 music/ring/alarm/call/system 等音频流) | set_volume(15, "music") |
| get_notifications | 获取通知列表(需在「设置 > 通知 > 通知使用权」开启本应用) | local n = get_notifications() |
| clear_notifications | 清除通知栏 | clear_notifications() |
| get_location | 获取GPS定位(需定位权限;取最近一次缓存定位,无缓存返回 nil) | local loc = get_location() |
| get_contacts | 获取联系人(需通讯录权限,最多 500 条) | local c = get_contacts() |
| get_sms_list | 获取短信列表(需短信权限,默认 20 条 / 上限 500) | local s = get_sms_list(10) |
| send_sms | 发送短信(未授予短信权限时自动拉起短信界面预填内容) | send_sms("号", "内容") |
| make_call | 拨打电话(未授予通话权限时拉起拨号盘预填号码) | make_call("10086") |
| get_sim_info | 获取SIM卡信息(手机号 / IMEI / 订阅号需电话权限,未授权返回空串) | local s = get_sim_info() |
| get_wifi_list | 获取WiFi列表(按信号强度排序;Android 6.0+ 需定位权限) | local w = get_wifi_list() |
| connect_wifi | 连接WiFi(root 设备成功率最高;Android 10+ 未 root 受系统限制) | connect_wifi("名", "密码") |
| get_bluetooth_devices | 获取已配对蓝牙设备(Android 12+ 需蓝牙权限) | local b = get_bluetooth_devices() |
| get_file_list | 获取文件列表(已实现;返回 name / path / size / is_dir / is_file 的文件信息表数组) | local f = get_file_list("/sdcard/") |
| download_file | 下载文件到设备(已实现;HTTP 下载,相对路径=App 内置 scripts 目录 / 代码箱当前工程目录) | download_file(url, path) |
| upload_file | 上传文件到服务器(已实现;multipart/form-data,字段名 file,返回是否成功) | upload_file(path, url) |
| delete_file | 删除文件/目录(已实现;目录会递归删除;返回 boolean,拼字符串必须 tostring) | log("结果: " .. tostring(delete_file("data.txt"))) |
| create_dir | 创建目录(已实现;等价 mkdir,可递归创建父目录) | create_dir("/sdcard/app") |
| file_exists | 检查文件/目录是否存在(已实现;绝对路径直接检查,相对路径=App 内置 scripts 目录) | if file_exists("/sdcard/Download/a.txt") then ... |
| read_file_content | 读取文件内容(已实现,与read_file等价;相对路径=App 内置 scripts 目录) | local c = read_file_content("config.json") |
| write_file_content | 写入文件内容(已实现,与write_file等价;相对路径=App 内置 scripts 目录) | write_file_content("data.txt", "内容") |
| get_sdcard_path | 获取SD卡路径 | get_sdcard_path() |
| get_internal_storage_path | 获取内部存储路径 | get_internal_storage_path() |
| get_app_data_path | 获取应用数据目录路径 | get_app_data_path() |
| get_cache_path | 获取应用缓存目录路径 | get_cache_path() |
| get_download_path | 获取下载目录路径 | get_download_path() |
| get_pictures_path | 获取图片目录路径 | get_pictures_path() |
| get_music_path | 获取音乐目录路径 | get_music_path() |
| get_movies_path | 获取视频目录路径 | get_movies_path() |
| get_dcim_path | 获取相机照片目录路径 | get_dcim_path() |
| get_documents_path | 获取文档目录路径 | get_documents_path() |
| get_ringtone_path | 获取铃声目录路径 | get_ringtone_path() |
| get_alarms_path | 获取闹钟音频目录路径 | get_alarms_path() |
| get_notifications_path | 获取通知音频目录路径 | get_notifications_path() |
| get_podcasts_path | 获取播客目录路径 | get_podcasts_path() |
🔐 设备/系统 API 权限速查(古龙精灵 / 古龙代码箱):相关权限已在 AndroidManifest 中声明 —— 定位 ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION,WiFi ACCESS_WIFI_STATE / CHANGE_WIFI_STATE,通讯录 READ_CONTACTS,短信 READ_SMS / SEND_SMS,电话 CALL_PHONE / READ_PHONE_STATE / READ_PHONE_NUMBERS,蓝牙 BLUETOOTH / BLUETOOTH_ADMIN / BLUETOOTH_CONNECT。运行时权限未授予时接口不会抛异常:返回 ""、空表、false 或 nil,并写入日志,脚本可按返回值降级处理。
📌 特例说明:get_notifications 需在「设置 > 通知 > 通知使用权」手动开启本应用,否则只能读到本应用自己的通知;get_location 只读取最近一次缓存定位(无缓存返回 nil);get_wifi_list 在 Android 6.0+ 需定位权限才拿得到扫描结果;connect_wifi 在 Android 10+ 未 root 设备上受系统限制会返回 false(root 设备走系统 wifi 命令);get_bluetooth_devices 仅返回已配对设备,Android 12+ 需 BLUETOOTH_CONNECT;send_sms / make_call 无对应权限时会自动拉起短信界面 / 拨号盘预填内容(需手动点击发送或拨打)。
💡 read_file_content / write_file_content / file_exists 已在设备端(古龙精灵、古龙代码箱)实现:read 成功返回 string(内容行尾统一为 \n,CRLF 文本也会被归一化;若最后一行是空行会自动去掉,不会多出空行)、write 成功返回 true、file_exists 返回 true/false;文件不存在或失败返回 nil / false。示例统一使用相对路径,在「古龙精灵」中即以 App 内置 scripts 目录为基准(如 data.txt → App 内置 scripts 目录/data.txt),便于脚本打包发布。绝对路径同样支持;若路径无权限(如共享存储 /sdcard/Download),设备已 root 时会自动通过 su 回退读写与存在性判断。
🔎 绝对路径示例(雷电模拟器):write_file_content("/sdcard/Download/gulong/data.txt", "Hello") → read_file_content("/storage/emulated/0/Download/gulong/data.txt")(/sdcard 等价 /storage/emulated/0);判断是否存在:if file_exists("/sdcard/Download/gulong/data.txt") then ... end;雷电共享目录:/mnt/shared/Pictures(对应 Windows「文档\LDPlayer\Pictures」,电脑端可直接查看)。
🧾 文件管理 API已在安卓端(古龙精灵、古龙代码箱)实现:delete_file / create_dir(别名 mkdir)/ list_files / get_file_list / copy_file / move_file / download_file / upload_file;路径规则与 read/write 完全一致:绝对路径直接操作(App 沙箱无权限时,设备已 root 会自动用 su 回退),相对路径在「古龙精灵」= App 内置 scripts 目录、在「古龙代码箱」= 当前工程目录。
⚠️ 返回值与日志(常见坑):delete_file / create_dir / copy_file / move_file / download_file / upload_file 这些接口返回的都是 boolean(true = 成功,false = 文件/目录不存在或失败),直接拼进字符串会报 attempt to concatenate string and boolean(典型报错行:log("结果: " .. delete_file("data.txt"))),必须写 local ok = delete_file("data.txt") → log("结果: " .. tostring(ok)),或直接 log("结果: " .. (ok and "删除成功" or "删除失败/文件不存在"));delete_file 对目录会递归删除。另外「古龙代码箱」的 log 只接收第一个参数(多写的参数会被忽略),所以「先 tostring 拼成一个字符串,再 log」在古龙精灵 / 古龙代码箱 都安全。
📱 设备方法 19个API
-- 默认顶部显示 show_message("任务开始执行") -- 预设位置 + 时长 show_message("任务完成!", "bottom", 5000) -- 坐标定位:(x,y) 为消息框中心(自动缩放适配分辨率) show_message("⚠️ 血量不足,请注意!", 600, 300, 3000) -- 坐标定位(table 写法) show_message("检测到BOSS", {x = 640, y = 200}, 2000)
toast("操作完成")
runApp("com.tencent.mm") stopApp("com.tencent.mm")
-- ============================================================ -- clearAppData(包名) -> boolean -- 清空应用「全部数据」= 系统 设置 → 应用 → 存储 → 清除数据: -- 登录账号 / SharedPreferences / 数据库 / files / cache 全部删除, -- 目标应用回到「刚安装」状态,下次打开需要重新登录(换号、清缓存常用)。 -- 需要 root 或 App 是设备所有者(Android 9+);普通未 root 设备返回 false。 -- ============================================================ -- 用一个函数收口,避免「单个清 / 批量清」写两遍 local function clearAndRelaunch(pkg, relaunch) if pkg == nil or pkg == "" then log("包名为空,跳过") return false end -- ① 目标在跑就先关掉:个别机型「边写边清」会清不干净(pm clear 内部也会 force-stop,这里只是兜底) stopApp(pkg) sleep(300) -- ② 清数据:系统确认成功才返回 true if not clearAppData(pkg) then log("❌ " .. pkg .. " 清除失败:包名写错 / 没有 root 且不是设备所有者 / 目标不存在") return false end log("✅ " .. pkg .. " 已清除(账号已退出),下次打开需要重新登录") -- ③ 要重开就等系统把数据目录清干净再启动,清完立刻 runApp 有可能起不来 if relaunch then sleep(1500) runApp(pkg) end return true end -- 单个游戏:清数据 → 重新打开(重开后会停在登录页) clearAndRelaunch("com.example.game", true) -- 批量清理(模拟器多开 / 换号常用):只清不重开,最后统一报告结果 local pkgs = { "com.example.game", "com.example.game2", "com.example.game3", } local okCount = 0 local failList = {} for i = 1, #pkgs do if clearAndRelaunch(pkgs[i], false) then okCount = okCount + 1 else failList[#failList + 1] = pkgs[i] end sleep(300) -- 批量时给系统一点缓冲,避免连续清理互相打架 end log(string.format("批量清理完成:成功 %d / 共 %d", okCount, #pkgs)) if #failList > 0 then log("失败:" .. table.concat(failList, ", ")) end -- 防呆:包名为空 / 带空格、分号等字符会直接返回 false,不会拼进 root 命令 log(tostring(clearAppData())) -- false log(tostring(clearAppData("com.foo;reboot"))) -- false
-- 1) 改完需要冷启动才生效的配置,让 App 自己重启(重启后手动点「启动脚本」) restartApp() -- 这一行后面的代码不会执行 -- 2) 重启完自动继续跑(改完截图模式 / 输入法 / 包内脚本后用这个) show_message_keep("配置已保存,2 秒后自动重启并继续运行...", 600, 300) sleep(2000) restartApp(true) -- 新进程起来后自动帮你点一次「启动脚本」 -- 3) 放进停止回调:收尾 + 重启(并自动继续运行)一条龙 setStopCallBack(function(error) imeLib.unlock() -- 解锁古龙输入法 close_message_keep() -- 关掉驻留消息窗 restartApp(true) -- 收尾完成后重启,并自动继续运行 end)
local pkg = frontAppName() if appIsFront("com.example.game") then log("游戏在前台") end
writePasteboard("123") local text = readPasteboard()
local num = rnd(1, 100) local x = rnd(100, 500) -- 随机点击位置
local w, h = getDisplaySize() tap(w / 2, h / 2) -- 点击屏幕中心
setControlBarPosNew(0, 0) -- 左上角 setControlBarPosNew(1.0, 0) -- 右上角 setControlBarPosNew(0.5, 0.5) -- 屏幕中心
local y, m, d, h, mi, s = get_current_time() log(string.format("%d-%02d-%02d %02d:%02d:%02d", y, m, d, h, mi, s))
local hours = get_uptime() / 3600000
x (number, 可选): 第1行的 X 坐标(脚本设计分辨率,自动缩放适配实际屏幕);与 y 同时给出时才生效
y (number, 可选): 第1行的 Y 坐标(窗口底边位置)
line (number, 可选): 行号 1~5,默认 1
提示:只传一个数字时(如 show_message_keep(msg, 3))该数字视为行号;也可用 table 写法 {x=, y=, line=}
-- 1) 创建并显示第1行(默认),坐标 (600,300) 为第1行位置
show_message_keep("状态: 运行中", 600, 300)
-- 2) 更新第1行内容(其他行不受影响)
show_message_keep("状态: 打怪中 12:30")
-- 3) 第2行(显示在第1行上面),坐标省略 → 沿用上次位置
show_message_keep("血量: 980/1200", nil, nil, 2)
-- 4) 第3行写在第2行上面
show_message_keep("金币: 12,345", nil, nil, 3)
-- 5) 只给一个数字 → 视为行号(更新第4行)
show_message_keep("经验: 15%", 4)
-- 6) table 写法:位置 + 行号一起指定
show_message_keep("BOSS 已刷新", {x = 600, y = 300, line = 5})
-- 7) 清空某一行(例如 BOSS 消失后)
show_message_keep("", nil, nil, 5)
-- 8) 用完后销毁整个驻留消息窗
close_message_keep()
-- ===== 典型循环用法(10秒刷新一次血量,全程不闪屏)=====
show_message_keep("自动挂机中", 60, 400) -- 第1行:固定位置
while true do
local hp = get_color(100, 100) -- 你自己的取值逻辑
show_message_keep("血量: " .. tostring(hp), nil, nil, 2)
show_message_keep("金币: " .. tostring(os.time()), nil, nil, 3)
sleep(1000)
end
-- 脚本正常结束时记得清理:close_message_keep()
-- 9) 推荐收尾:只要用了驻留窗,就顺手注册一个停止回调,
-- 这样「手动点停止 / 脚本跑完 / 脚本报错退出」都会自动把窗口关掉(不会残留在屏幕上)
setStopCallBack(function(error)
imeLib.unlock() -- 解锁古龙输入法,恢复系统输入法
close_message_keep() -- 关闭驻留消息窗
execRoot("settings put global policy_control null") -- 恢复状态栏显示
end)-- 先显示驻留面板
show_message_keep("自动挂机中", 60, 400)
show_message_keep("血量: 1200/1200", nil, nil, 2)
-- 任务结束时关闭
close_message_keep()
show_message("任务完成!", "bottom", 3000) -- 需要短暂提示可以用 show_message
-- ===== 推荐:写进停止回调,脚本无论怎么结束都会自动关闭 =====
-- 手动点「停止」时,脚本末尾那行 close_message_keep() 根本走不到,窗口会残留在屏幕上;
-- 放进 setStopCallBack 后:手动停止 / 脚本跑完 return / 脚本报错退出,都会自动关掉
setStopCallBack(function(error)
imeLib.unlock() -- 解锁古龙输入法,恢复系统输入法
close_message_keep() -- 关闭驻留消息窗
execRoot("settings put global policy_control null") -- 恢复状态栏显示
end)options (string|table, 可选): 传字符串 = 窗口标题;传 table 可精细设置:
title (string): 标题,默认「提示」
text (string): 正文(也可直接用第 1 个参数)
stop (boolean): 是否显示左下角「停止脚本」,默认 true
resume (boolean): 是否显示右下角「继续运行」,默认 true
stop_text (string): 停止按钮文字,默认「停止脚本」
resume_text (string): 继续按钮文字,默认「继续运行」
stop_action (string): 点「停止脚本」后的动作,'stop'=立即停止脚本(默认)/ 'return'=只返回 false,由脚本自行处理
timeout (number): 超时毫秒数,0=不限时(默认 0)
timeout_action (string): 超时后的动作,'resume'(默认,继续运行)/ 'stop'(停止脚本)
position (string): 'center'(默认)/ 'top' / 'bottom'
x, y (number): 坐标定位,表示窗口中心,按脚本设计分辨率自动缩放适配实际屏幕
text_size (number): 正文字号,默认 16
-- 1) 最简用法:两个按钮都显示,脚本暂停等你选择
local ret = show_dialog("请手动完成登录,完成后点「继续运行」")
if ret then
log("用户选择继续,脚本继续执行")
end
-- 2) 自定义标题(第 2 个参数传字符串)
show_dialog("检测到验证码,请手动输入", "人工确认")
-- 3) 只显示「继续运行」(隐藏左下角停止按钮)
local ok = show_dialog("请确认背包已清空", {stop = false})
-- 4) 只显示「停止脚本」,并自定义按钮文字
show_dialog("账号异常,脚本需要停止", {resume = false, stop_text = "退出脚本"})
-- 5) 超时自动继续(5 秒没操作就继续跑)
show_dialog("BOSS 即将刷新,是否等待?", {timeout = 5000, timeout_action = "resume"})
-- 6) 坐标定位 + 不显示任何按钮(点按弹窗任意位置继续)
show_dialog("脚本已暂停(点我继续)", {stop = false, resume = false, x = 640, y = 360})
-- 7) 典型用法:异常时询问用户,按选择决定去留
local ret = show_dialog("检测到角色死亡,\n是否继续挂机?", {
title = "运行提示",
stop_text = "停止脚本",
resume_text = "继续挂机"
})
if ret then
log("继续挂机")
else
log("用户选择停止")
end
-- 8) 想自己做善后处理:用 stop_action="return" 拿到 false,再决定去留
local ret = show_dialog("检测到异常,是否继续?", {
stop_action = "return" -- 点「停止脚本」时只返回 false,平台不强制停止
})
if ret then
log("继续执行")
else
log("用户要求停止,做善后处理后自己结束脚本")
close_message_keep() -- 例如:清理驻留面板
return
end🔐 Root与Shell 3个API
-- 查看系统信息 local info = execRoot("cat /proc/version") print("系统版本: " .. info) -- 获取当前用户 print("当前用户: " .. execRoot("whoami")) -- 查看进程列表 print(execRoot("ps -A")) -- 隐藏状态栏(完全沉浸式,* 表示所有应用) execRoot("settings put global policy_control immersive.full=*") -- 恢复状态栏显示 execRoot("settings put global policy_control null")
-- 查看当前已隐藏的状态栏图标 local hidden = shell("settings get secure icon_blacklist") print("已隐藏图标: " .. hidden) -- 隐藏蓝牙与QQ图标(无需root,Android 6.0+) shell("settings put secure icon_blacklist bluetooth,qq") -- 查看所有支持隐藏的图标名称 print(shell("cmd statusbar get-status-icons")) -- 隐藏所有应用的状态栏(如需写权限请改用 execRoot) shell("settings put global policy_control immersive.status=*")
模拟器 / 虚拟机·2026-10-26 起默认不再重启系统(显式 mode="soft" 除外,详见下方 mode 四档):检测到跑在模拟器 / 虚拟机里(雷电 / MuMu / 夜神 / AVD)时,本函数改为「重启古龙自身进程」——停脚本收尾 → 预约拉起自身 → 退出进程,几秒回到主界面,安卓画面一秒都不停,adb / 投屏也不会断;continue_run=true 照旧自动继续运行,同样返回 true,且不需要任何权限。原因:软重启(sync; setprop ctl.restart zygote)客机 60~90 秒就能恢复,但宿主 LDPlayer 的渲染通道会停在重启前那一帧、不会自己恢复(实测 4 分钟以上,只有宿主窗口失焦再得焦才刷新),后台窗口受 Windows 前台锁限制唤不醒;硬重启(su -c "sync; reboot")更糟,雷电会把宿主里的 VM 一起 poweroff,得人工 ldconsole launch。
真机:需要设备已 root(su 授权)、App 是设备所有者,或 App 持有系统 REBOOT 权限三者之一;普通未 root 设备无法重启系统。mode 四档:auto(默认:模拟器 / 虚拟机重启古龙自身进程,真机整机重启)/ app(任何设备都只重启古龙自身进程,画面一秒不停,不需要 root 或任何权限)/ soft(软重启安卓框架 ctl.restart zygote,模拟器 / 云手机 / 真机都照做,需要 root;没有 root 时自动降级:模拟器 / 云手机 → app,真机 → hard)/ hard(强制整机重启;模拟器上有宿主把 VM 一起断电的风险)。调用后先走一遍正常停止流程(setStopCallBack、解锁古龙输入法、释放按下的手指、上报停止原因),再在 delay 指定的延迟后执行。真机上返回 true 只表示请求已受理(随后关机,函数之后的代码通常不执行);模拟器上返回 true 表示约 delay 毫秒后重启自身进程。
opts 表(第 5 个参数):把软重启的系统侧收益补回给「重启自身进程」;wake_screen / settle_ms / restart_surfaceflinger 在 soft / hard 档同样生效。clean_memory(重启前 sync + drop_caches 回收页缓存)/ kill_background(重启前 am kill-all 清可杀后台)/ reset_network(重启前 wifi / 数据关再开)/ wake_screen(重启后亮屏 + 解锁 + 回桌面 + 插电常亮;最多重试 3 次,用 isInteractive / isKeyguardLocked 判断是否真的亮屏解锁,治「不点一下画面就永远不进系统」;系统级重启 soft / hard 默认开、app 档默认关,显式写了按你的;需要 su,没 root 自动跳过)/ settle_ms(仅 app 档生效:重启后延迟多久再自动启动脚本,0~30000;soft / hard 由公共设置的等待秒数决定)/ restart_surfaceflinger(soft 档顺带 ctl.restart surfaceflinger,实验:缓解模拟器宿主停帧,会黑屏更久)。前三个「重启前清理」只在 app 档执行,soft / hard 本身就是整机级重启;所有选项失败都只写日志,不会卡住重启。
重启后的开机启动(两条链路,别混用):app 档(只重启自身进程)沿用原来的「自动继续运行」——停脚本 → 预约拉起自身 → 3 秒 + settle_ms 后自动启动脚本,没有倒计时(画面没中断,不需要等)。soft / hard 档(真的重启了安卓系统)不再由本函数直接拉起脚本,改走「公共设置 → 开机启动脚本」的倒计时流程:开机后控制面板自动打开 → 按公共设置里的等待秒数显示倒计时(0~600 秒,用户面板「设置 → 公共设置」可改)→ 到点启动脚本并关闭面板;公共设置没开启就只打开面板、不启动脚本(这时 continue_run 也不会绕过它)。古龙精灵会在脚本触发的系统重启后强制走这条流程,并以服务端公共设置为权威值(本机快照过期也不会漏);古龙代码箱没有公共设置链路,系统级重启仍按 continue_run 自动继续。settle_ms 只对 app 档有意义。
防重启循环:90 秒内只受理一次(受理时把受理时刻落盘,不区分开机周期):冷却期内的第二次调用直接返回 false,所以「重启 → 自动继续运行 → 立刻又执行到 rebootSystem()」不会无限循环;满 90 秒即正常放行,同一次开机里也不会被永久锁死。同机装了精灵 + 代码箱时闸门是设备级共享的(/data/local/tmp/gulong_reboot_guard,需 root)。
模拟器 / 云手机·整机重启:guest 里 reboot 只是向宿主发关机请求,宿主 poweroff 再冷启,中间黑屏 30~120 秒属正常(实测雷电 Android 14 约 90 秒),不是死机 —— 这也是模拟器上默认不走这条路的原因。确实要整机重启模拟器请用宿主命令(雷电 ldconsole quit/launch --index,MuMu MuMuManager control -v 0 reboot,夜神 NoxConsole reboot)。PC 引擎(本机调试运行)不实现 rebootSystem,调用返回 false 并在日志提示。
-- 1) 立即重启(默认 1 秒后),重启后手动重新启动脚本 local ok = rebootSystem() if not ok then log("当前设备无法重启:需要 root / 设备所有者 / 系统签名") end -- 2) 留 3 秒收尾,重启后自动继续跑(无人值守脚本推荐) show_message_keep("设备将在 3 秒后重启…", 600, 300) rebootSystem(3000, true) -- 真机上这一行通常不会执行:delay 到点后脚本会被停止并重启设备 -- 模拟器 / 虚拟机上会重启古龙自身进程,系统不重启、画面不停 -- 3) 防重启循环:90 秒内只受理一次,冷却期内的第二次一定返回 false -- (重启后自动继续运行、脚本又跑到这里时,不会把设备再重启一遍) local again = rebootSystem() if not again then log("距上次受理重启不到 90 秒,这次不重启") end -- 4) 模拟器 / 虚拟机上不会重启系统:改为重启古龙自身进程(画面一秒不停), -- 顺带把游戏 force-stop,下次进游戏就回到登录 / 主界面 rebootSystem(2000, true, "auto", "com.xxx.game") -- 5) 只想重启古龙自身进程:任何设备都画面不停,也不需要 root rebootSystem(1000, true, "app") -- 6) 软重启安卓框架 + 重启后亮屏解锁(需要 root) -- 没有 root 时自动降级为 app / hard,日志里会写明 rebootSystem(2000, true, "soft", nil, { wake_screen = true, -- soft / hard 档默认就是 true,显式写更清楚 settle_ms = 5000, -- 仅 app 档生效;soft / hard 由公共设置倒计时决定 clean_memory = true, -- 重启前顺手回收内存缓存 kill_background = true -- 重启前清一次后台进程 }) -- 7) 参数多时可以用单表形式,含义和位置参数完全一样 rebootSystem{ delay_ms = 3000, continue_run = true, mode = "app", stop_app = { "com.xxx.game", "com.yyy.game" }, opts = { settle_ms = 3000 } }
🌐 代理 IP(换 IP 防同 IP 限号) 6个API
游戏常按「同一个公网 IP 能登录几个账号」限号。这组 API 从携趣(xiequ.cn)提取短效代理,
写进模拟器的系统代理(settings put global http_proxy),
再重启目标 App,让它用新的出口 IP 连服务器。六个函数可以拆开组合,也可以只用
switch_proxy_ip 一步到位。
⚠️ 仅供学习交流,请勿用于真实游戏。
· 携趣账号 + 提取链接,且本机公网 IP 已加入白名单:携趣后台「API 提取」页复制完整链接; 白名单认的是本机出口 IP(
curl ifconfig.me 查)。没加白名单时接口返回 DB1 之类的错误文本,脚本只会拿到 nil。· 设备能写 settings:需要 root 或 shell 有
WRITE_SECURE_SETTINGS;否则日志报
❌ 写入系统代理失败:期望 1.2.3.4:8888,实际读取到 ''。· 目标 App 遵守 Android 系统代理,且换完必须重启进程:OkHttp / HttpURLConnection / WebView / UnityWebRequest 默认实现有效;自研 TCP/UDP 长连接无效(见本节末尾「局限与替代方案」)。
📱 设备端签名(代码箱调试 / 已装 APK 完全一致)
| 函数 | 签名 | 作用 |
|---|---|---|
| set_proxy | (ip, port, package) | 写设备系统代理,可顺带重启 App |
| get_proxy | () | 读设备当前系统代理 |
| clear_proxy | (package) | 清除系统代理,可顺带重启 App |
| fetch_proxy | (api_url, num) | 只提取,不动设备 |
| switch_proxy_ip | (api_url, package, num, verify, timeout) | 提取 → 校验 → 写入 → 重启 |
| proxy_out_ip | (proxy, timeout) | 查出口 IP |
脚本统一在设备端执行(「📦 代码箱调试」与安装在手机/模拟器上的「古龙精灵」「古龙代码箱」签名完全相同),
开发工具只负责把项目发到设备运行。所以这里没有 adb 参数 —— 代理直接写脚本所在设备的系统代理。
fetch_proxy / switch_proxy_ip
不传 api_url 时会退到内置的 http://api.xiequ.cn/VAD/GetIp.aspx
(不含 uid/vkey,通常必失败)。设备上既没有 proxy.json、也不读
GULONG_XQ_API_URL,链接只能写在脚本里、项目文件里,或从 get_setting 读(见本节末尾)。
settings get global http_proxy 读到的值与写入值完全一致才返回 true。传 package 时会
force-stop + monkey 启动目标 App —— 系统代理只在进程启动时读取,不重启的话老进程仍走旧网络。-- ① 已有 "ip:port"(最常用) set_proxy("1.2.3.4:8888") -- ② IP 和端口分开 set_proxy("1.2.3.4", 8888) -- ③ 写完顺便重启游戏 set_proxy("1.2.3.4:8888", nil, "com.xxx.game") -- ④ table 写法:把 fetch_proxy 拿到的地址直接丢进来 local addr = fetch_proxy() if addr then set_proxy({ip = addr, package = "com.xxx.game"}) end
settings get global http_proxy)。null / 空 /
:0 一律视为直连并返回 nil。返回的 table 里 address 是 "ip:port",host / port 已经拆好,
可直接用于打日志或判断「是不是已经换过 IP」。-- 看看现在走的是直连还是代理 local p = get_proxy() if p then log(string.format("当前代理 %s(%s:%d)", p.address, p.host, p.port)) else log("当前直连") end -- 换号前先判断:已经是目标代理就不用重设(省时间也省代理时长) local target = "1.2.3.4:8888" if not p or p.address ~= target then set_proxy(target, nil, "com.xxx.game") sleep(8000) end
http_proxy :0 兜底)。
清掉的键:http_proxy、https_proxy、global_http_proxy_host、
global_http_proxy_port、global_http_proxy_exclusion_list、
global_proxy_pac_url。传 package 时清完顺带重启该 App,让它立刻走回直连。-- ① 只清代理(脚本收尾用,避免影响别的软件) clear_proxy() -- ② 清完并重启游戏,立刻恢复直连 clear_proxy("com.xxx.game") -- ③ 这个号被限制:先摘代理 → 重新换一个 IP → 再登录 clear_proxy("com.xxx.game") sleep(5000) set_proxy("5.6.7.8:9999", nil, "com.xxx.game") sleep(8000) login(username, password)
num 改成脚本要的个数,
其它参数原样保留。api_url 必传(设备端不读环境变量 / 配置文件,不传会退到不含 uid/vkey 的
默认地址,通常必失败)。接口返回纯文本 ip:port 列表(换行 / 逗号 / 分号 / 空格分隔)
或 JSON(携趣「提取格式」选 JSON、或链接里带 plat=0,形如
[{"IP":"1.2.3.4","Port":"8888"}])都能解析。num > 1 时接口会返回多个,
本函数只返回第一个;想逐个挑选请用 switch_proxy_ip(它会挨个校验)。-- ① 用自己的提取链接(设备端必须显式传) local addr = fetch_proxy(API_URL) if addr then log("提到代理: " .. addr) end -- ② 用自己的链接,一次提 3 个(只取第一个) local one = fetch_proxy("http://api.xiequ.cn/VAD/GetIp.aspx?act=get&uid=xxx&vkey=xxx&num=3&spl=1", 3) -- ③ 提取 + 写入 + 重启,自己控制流程(等价于 switch_proxy_ip) -- API_URL = 你的携趣提取链接常量(见本节「提取链接配置」) local addr2 = fetch_proxy(API_URL) if addr2 then set_proxy(addr2, nil, "com.xxx.game") sleep(10000) login(username, password) end
plat=0)两种返回。-- ① 传链接 + 重启游戏 local addr = switch_proxy_ip(API_URL, "com.xxx.game") if addr then log("已换 IP: " .. addr) sleep(10000) -- 等游戏起来 else log("换 IP 失败,本次直连登录") end -- ② 用自己的链接、跳过校验(快,但可能拿到死 IP) switch_proxy_ip(API_URL, "com.xxx.game", 1, false) -- ③ 每个账号换一次 IP for i, account in ipairs(accounts) do local one = switch_proxy_ip(API_URL, "com.xxx.game", 2, true, 15) if not one then log(string.format("[%d/%d] 换 IP 失败,跳过 %s", i, #accounts, account)) else log(string.format("[%d/%d] %s 走 %s", i, #accounts, account, one)) sleep(10000) login(account, passwords[i]) sleep(30000) -- 做完任务 end end
https://www.xiequ.cn/OnlyIp.aspx:
不传参数查脚本所在设备的出口 IP(即手机 / 模拟器当前网络);
传 "ip:port" 或 table(可带账密)则经该代理去查。失败返回空字符串,不抛异常。-- ① 查本机出口 IP(做对比基线) log("本机出口: " .. proxy_out_ip()) -- ② 验证刚提取的代理能不能出网 local a = fetch_proxy(API_URL) if a then local ip = proxy_out_ip(a) log(ip == "" and ("代理不可用: " .. a) or ("出口 IP: " .. ip)) end -- ③ 换 IP 前后对比 local before = proxy_out_ip() local addr = switch_proxy_ip(API_URL, "com.xxx.game") log(string.format("换 IP: %s -> %s", before, proxy_out_ip(addr))) -- ④ table 写法:带账密(携趣隧道代理) log(proxy_out_ip({host = "1.2.3.4", port = 8888, user = "u", pass = "p"}))
⚙️ 提取链接怎么放进脚本(设备端三选一)
| 方式 | 写法 | 适用 |
|---|---|---|
| ① 脚本内常量 | local API_URL = "http://api.xiequ.cn/...uid=xx&vkey=xx..." | 最简单,但链接会随脚本一起分发 |
| ② 项目内文件 | local API_URL = read_file_content("proxy_api.txt") | 链接与代码分离,改链接不用改脚本(文件随项目一起下发到设备) |
| ③ 服务端设置 | local API_URL = get_setting("xq_api_url", "") | 推荐:链接放在用户面板「脚本设置」里,按用户区分,不进脚本包 |
⚠️ 设备端没有任何「全局代理配置」:没有 proxy.json,也不读 GULONG_XQ_API_URL /
GULONG_XQ_UID / GULONG_XQ_VKEY 环境变量。不传链接时只会退到内置的
http://api.xiequ.cn/VAD/GetIp.aspx(不含 uid/vkey,通常必失败),日志里会打印接口返回的错误文本。
📌 只改写 num:脚本传入的 num 会覆盖链接里的 num,其余参数(uid / vkey / type / addr / spl)
原样保留 —— 直接把携趣后台复制的链接粘进来即可,不用手工拼参数。
🧩 完整示例:每个账号换一次 IP
-- 同一份脚本在「📦 代码箱调试」与已安装 APK 上都能跑 local API_URL = "http://api.xiequ.cn/VAD/GetIp.aspx?act=get&uid=xxx&vkey=xxx&spl=1" local GAME_PKG = "com.xxx.game" local accounts = {"acc1", "acc2", "acc3"} local function login(account, password) log("登录 " .. account) -- 换成自己的登录 / 起号逻辑 end function main() for i, account in ipairs(accounts) do -- ① 提取 → 校验 → 写系统代理 → 重启游戏(返回真正生效的地址) local addr = switch_proxy_ip(API_URL, GAME_PKG, 2, true, nil, 15) if addr then log(string.format("[%d/%d] %s 走 %s", i, #accounts, account, addr)) log("代理出口: " .. proxy_out_ip(addr)) -- ② 确认真的换了 else log(string.format("[%d/%d] 换 IP 失败,直连继续", i, #accounts)) end sleep(10000) -- 等游戏启动完成 login(account, "password") sleep(30000) -- 做完任务 end -- ③ 收尾:摘掉系统代理,别影响设备上的其它联网软件 clear_proxy(GAME_PKG) end main()
🚫 局限与替代方案
| 情况 | 表现 | 建议 |
|---|---|---|
| 游戏用自研 TCP / UDP 长连接 | 代理写入成功,游戏内显示的 IP 却没变 | 系统代理天然不接管原始 socket;需 root + tun2socks / redsocks,或用携趣 PC 版「进程代理」整机转发 |
| 换 IP 后没有重启 App | 仍走旧 IP | 传 package(脚本自动 force-stop + 启动),或手动重启一次 App |
| 代理出口是 NAT 同一 IP | 换了好几次,出口 IP 都一样 | 携趣后台换线路(type)或指定地区(addr),再重新提取 |
| 系统代理是全局的 | 设备上其它联网 App 也会走代理 | 脚本收尾调用 clear_proxy(package) |
| 代理需要账密 | 写进系统代理后 App 连不上 | Android 静态系统代理不支持账密认证;携趣改用「本机 IP 白名单」,或用隧道代理(账密在隧道入口校验) |
| Android 端写本机设置失败 | 日志:❌ 写入系统代理失败 / 需要 root | 给设备 root;或 adb 授予 pm grant 古龙精灵包名 android.permission.WRITE_SECURE_SETTINGS |
· 读当前代理:
adb shell settings get global http_proxy(输出 null = 直连)· 写 / 清代理:
adb shell settings put global http_proxy 1.2.3.4:8888,清除用 adb shell settings put global http_proxy :0· 确认代理本身可用:
curl -x http://1.2.3.4:8888 https://www.xiequ.cn/OnlyIp.aspx· 确认 App 是否重启过:
adb shell pidof com.xxx.game(换 IP 前后 pid 应变化或短暂为空)
proxy_out_ip("ip:port") 确认代理能用 →
再用 get_proxy() 确认地址真的写进设备 → 再看日志里有没有 ✅ 已重启: 包名 →
最后判断游戏是不是走 TCP 直连(见上表第一行)。
🧠 Memory学习(进程内存读写) 3个API
用 GetModuleHandle / MemoryRead / MemoryWrite 按「基址 + 偏移」读写安卓应用(含手游)的进程内存,用于学习逆向分析、研究数值在内存里的存放结构。三个 API 分工:GetModuleHandle 取模块基址(读写的起点)、MemoryRead 按地址读(整数 / 浮点 / 字符串 / 原始字节)、MemoryWrite 按地址写并自动回读校验。只在设备端提供(✅ 装在手机/模拟器上的「古龙精灵」「古龙代码箱」、✅「📦 代码箱调试」)。
① libunity.so 是什么?「模块」就是随 App 一起加载的
.so 动态库文件;libunity.so 是 Unity 引擎的运行时库(Unity 手游的核心模块,逆向时一般从它入手)。同类常见模块:libil2cpp.so(Unity 用 IL2CPP 把 C# 编成的原生代码,游戏数值多半在这里)、libmono.so(老版本 Unity 的 Mono 运行时)、libUE4.so(虚幻引擎)、libcocos2dlua.so / libcocos2djs.so / libcocos.so(Cocos2d-x)、libc.so / libm.so / libc++_shared.so(系统与 C/C++ 运行库,适合练手)。每款游戏用的引擎不同、模块名也不同:脚本里的 libunity.so 只是示例,请换成目标游戏里真实存在的 so 文件名。② 模块名怎么查?先
execRoot("pidof 包名") 拿到进程 pid,再执行 execRoot("cat /proc/<pid>/maps | awk '{print $6}' | sort -u | grep '[.]so$'") 列出该进程加载的所有 so,取路径最后一段文件名即可。③ 为什么必须先取基址?模块每次启动被加载到哪个地址都是随机的(ASLR),逆向工具里看到的「偏移」只有加上运行时基址才是真实地址:真实地址 = 模块基址 + 偏移;基址不要写死,脚本每次运行都要重新取。
🚀 典型流程:
GetModuleHandle() 取基址 → MemoryRead() 按偏移读取(多级指针就嵌套读)→ MemoryWrite() 写入并回读校验 → 用 HEX 看内存原貌核对类型与字节序。不确定读写链路通不通,先做一次冒烟测试:MemoryRead(GetModuleHandle("libc.so"), "HEX", nil, 4) 应得到 7F 45 4C 46(ELF 魔数)。⚠️ 使用要点(务必先看):① 需要 root:读写的是别的进程的内存,内部通过持久 su 通道 +
/proc/<pid>/maps、/proc/<pid>/mem 完成;无 root 时只能读写自身 App 进程(部分机型连自身也会被系统 SELinux 拦住);② 地址是十进制数值:安卓用户态地址 ≤
0x7FFFFFFFFFFF(约 1.4×10¹⁴),远小于 Lua 双精度能精确表示的 2⁵³,不丢精度;③ 偏移要自己分析:这组 API 只负责按地址读写,不做内存搜索 / 特征扫描,偏移来自静态分析(IDA/Ghidra)或动态调试;
④ 类型要写对:把 float 当 I32 读不会报错,只会得到「看起来像乱码」的数值,怀疑读错时用
HEX 看内存原貌;⑤ 合规提醒:修改别的应用数据可能导致其崩溃或账号异常,请仅在自己拥有合法授权的应用 / 测试环境里用于学习研究,不要用于破解、绕过反作弊或修改他人数据。
🧯 读不到 / 改不动?按顺序排查:① 包名、模块名是否写对(模块名要精确到
.so 全名且区分大小写);② 模块是否已经加载(不少游戏要进到主界面才加载 libil2cpp.so,可用循环 + sleep() 重试);③ 基址 / 偏移是否属于当前游戏版本(更新后偏移会变,基址每次启动都不同);④ 地址是否真的可读(先用 HEX 读 16 字节确认那块内存存在);⑤ 写入返回 false 时看脚本日志里的原因(只读页 / 回读不一致 / 没有 root);⑥ 写进去立刻被改回来,说明该字段不是数据源头,需要顺着指针链往上游找;⑦ 自身进程也读不到:检查 su 授权与机型 SELinux 限制。
进程内存读写(安卓端需 Root)(3个)
什么是模块:「模块」就是随 App 一起加载的 .so(ELF 动态库)文件,常见的有:
libunity.so —— Unity 引擎运行时(Unity 手游的核心模块,逆向时的第一入口)
libil2cpp.so —— Unity 的 IL2CPP 产物(C# 代码编译成的原生代码,游戏数值多半在这里)
libmono.so —— 老版本 Unity 的 Mono 运行时
libUE4.so —— 虚幻引擎(UE4 / UE5 手游)
libcocos2dlua.so / libcocos2djs.so / libcocos.so —— Cocos2d-x(Lua / JS / C++ 版)
libc.so / libm.so / libc++_shared.so —— 系统与 C/C++ 运行库(每台设备都有,适合练手)
libxxx.so —— 游戏自研模块,名字因游戏而异(必须写真实存在的文件名)
为什么必须先拿基址:模块每次启动被系统加载到哪个地址都是随机的(ASLR),逆向工具里看到的「偏移」只有加上运行时基址才是真实地址:真实地址 = 模块基址 + 偏移。
实现:按包名用 pidof 解析进程 pid(只传一个参数时用当前 App 自身进程)→ 读 /proc/PID/maps → 取该模块所有映射行里最小的起始地址(一个模块通常有 4~6 段映射:r--p / r-xp / rw-p 等,最小地址就是模块起点)。
匹配规则:只看「路径以 /模块名 结尾」的映射行,所以写 libunity.so 不会误命中 libunity.so.1 或别的路径下的同名文件。
需要 root(读的是别的进程);找不到进程 / 模块没加载 / 无 root 时返回 nil,原因会写进脚本日志。
moduleName (string): 模块名,要精确到 .so 全名,例如 "libunity.so" / "libil2cpp.so" / "libc.so";不能只写 "unity",也不能写 "libunity.so.1"。
参数形式:GetModuleHandle("包名", "模块名") → 目标应用进程(需要 root)
GetModuleHandle("模块名") → 当前 App 自身进程(免 root 也可能成功)
不确定模块名时,先列出目标进程加载的 so(完整写法见示例 3):
local pid = execRoot("pidof com.example.game"):gsub("%s+$", "")
log(execRoot("cat /proc/" .. pid .. "/maps | awk '{print $6}' | sort -u | grep '[.]so$'"))
打印建议 string.format("0x%X", base);若某端对十六进制报「number has no integer representation」,改用 string.format("%.0f", base)。
nil: 解析失败(失败原因同时写进脚本日志),常见原因:
· 包名写错 / 应用没在运行 → 找不到进程
· 模块名写错 / 模块还没加载(不少游戏要进到主界面才加载 libil2cpp.so)→ 进程里没找到模块
· 没有 root 或 su 授权被拒 → 读不到 /proc/PID/maps
-- ===== 1) 最简用法:拿基址 + 判断失败 =====
local base = GetModuleHandle("com.example.game", "libunity.so")
if base == nil then
log("取模块基址失败:请检查包名 / 模块名 / root 权限(原因见日志上一行)")
return
end
log(string.format("libunity.so 基址 = 0x%X(十进制 %.0f)", base, base))
-- ===== 2) 拿到基址后按偏移读写(偏移来自逆向分析,下面这些都是编造的示例值) =====
local hpAddr = base + 0x1A2B3C
log(string.format("血量地址 = 0x%X", hpAddr))
log("当前血量 = " .. tostring(MemoryRead(hpAddr, "I32")))
MemoryWrite(hpAddr, 99999, "I32")
log("修改后血量 = " .. tostring(MemoryRead(hpAddr, "I32")))
-- ===== 3) 模块名不确定?先列出目标进程加载的所有 so =====
local pid = execRoot("pidof com.example.game"):gsub("%s+$", "") -- 去掉末尾换行
if pid == "" then
log("进程没在运行(或没有 root,列不出别的进程)")
else
log(execRoot("cat /proc/" .. pid .. "/maps | awk '{print $6}' | sort -u | grep '[.]so$'"))
end
-- 输出示例(每行一个模块路径):
-- /data/app/~~xxxx/com.example.game-xxxx/lib/arm64/libunity.so
-- /data/app/~~xxxx/com.example.game-xxxx/lib/arm64/libil2cpp.so
-- 取路径最后一段文件名就是模块名:GetModuleHandle("com.example.game", "libil2cpp.so")
-- ===== 4) 一次取多个模块并缓存复用(推荐写法) =====
local Game = {}
Game.pkg = "com.example.game"
Game.unity = GetModuleHandle(Game.pkg, "libunity.so")
Game.il2cpp = GetModuleHandle(Game.pkg, "libil2cpp.so")
if Game.unity == nil then
log("libunity.so 没取到:确认已 root、且游戏已经启动")
return
end
log(string.format("unity = 0x%X,il2cpp = %s", Game.unity,
Game.il2cpp and string.format("0x%X", Game.il2cpp) or "未加载"))
-- 基址在「本次运行」里不会变:一个脚本只取一次、放进表里反复用(别放在每帧循环里反复调用)
-- ===== 5) 只传一个参数 = 当前 App 自身进程(免 root 也可能成功) =====
local selfLibc = GetModuleHandle("libc.so")
log("自身进程 libc.so 基址 = " .. tostring(selfLibc))
-- ===== 6) 冒烟测试:不碰游戏,验证「读内存」这条链路通不通 =====
local libc = GetModuleHandle("libc.so")
if libc then
-- ELF 文件头前 4 字节固定是 7F 45 4C 46(0x7F 后面跟 "ELF")
log("ELF 魔数 = " .. tostring(MemoryRead(libc, "HEX", nil, 4))) -- 期望输出 7F 45 4C 46
else
log("连自身 libc.so 都取不到:shell 通道 / 权限有问题")
end
-- ===== 7) 偏移集中管理,脚本更好维护 =====
local OFF = {
hp = 0x1A2B3C, -- 血量 (I32)
gold = 0x1A2B40, -- 金币 (U32)
posX = 0x1A2B50, -- 坐标 X (FLOAT)
posY = 0x1A2B54, -- 坐标 Y (FLOAT)
name = 0x1A2B60, -- 角色名 (STRING,最多 32 字节)
}
log("血量 = " .. tostring(MemoryRead(base + OFF.hp, "I32")))
log("金币 = " .. tostring(MemoryRead(base + OFF.gold, "U32")))
log(string.format("坐标 = (%.1f, %.1f)",
MemoryRead(base + OFF.posX, "FLOAT"), MemoryRead(base + OFF.posY, "FLOAT")))
log("角色名 = " .. tostring(MemoryRead(base + OFF.name, "STRING", nil, 32)))
-- ===== 8) 多级指针:基址 → 对象指针 → 字段(逆向里最常见的形态) =====
-- 例:base + 0x30 里存着玩家对象地址,对象 + 0x18 里存着血量
local p1 = MemoryRead(base + 0x30, "U64") -- 64 位进程指针是 8 字节用 U64;32 位进程用 U32
if p1 then
local p2 = MemoryRead(p1 + 0x18, "U64")
if p2 then
log("玩家血量 = " .. tostring(MemoryRead(p2 + 0x10, "I32")))
else
log("第二级指针为空:对象还没创建(进游戏 / 开一局后再读)")
end
end
-- ===== 9) 模块还没加载?循环重试 =====
local base2, tries = nil, 0
while base2 == nil and tries < 10 do
base2 = GetModuleHandle("com.example.game", "libil2cpp.so")
if base2 == nil then
tries = tries + 1
log("第 " .. tries .. " 次没取到,1 秒后重试…")
sleep(1000)
end
end
log("重试 " .. tries .. " 次后基址 = " .. tostring(base2))
-- ===== 10) 常见坑 =====
-- ① 基址只对「本次运行」有效:游戏重启 / 进程被杀后基址会变,脚本每次启动都要重新 GetModuleHandle。
-- ② 别把自己 log 出来的基址写死在脚本里,换台设备或重启就失效了。
-- ③ 包名不是应用名:用「应用管理」或 execRoot("pm list packages | grep 关键字") 确认真实包名。
-- ④ 模块名区分大小写:libUnity.so 和 libunity.so 不是同一个文件。
-- ⑤ 想要游戏没启动也能取基址是做不到的:模块必须先被加载进内存。最常用法:先 GetModuleHandle() 取模块基址,再加上逆向分析得到的偏移 → MemoryRead(基址 + 偏移, "I32")。
多级指针:把调用嵌套起来 —— 先读出「对象的地址」,再加偏移读出字段:
local obj = MemoryRead(base + 0x30, "U64") -- 第一级:读出指针
local hp = MemoryRead(obj + 0x18, "I32") -- 第二级:读出字段
小端字节序:安卓 / ARM 都是小端,同一份 4 字节 F4 01 00 00 按 U32 / I32 读就是 0x000001F4 = 500(低位字节在前);写入时本 API 也按小端编码,所以写入值和读回值能对上。
类型必须和内存里真实存放的一致:把 float 当 I32 / U32 读不会报错,只会得到「看起来像乱码」的数值(例如 1.5 的 float 按 I32 读是 1069547520)。怀疑读错时用 HEX 看内存原貌核对。
注意:这是「按已知地址读」,不做内存搜索 / 特征扫描;想找未知数值请先用内存搜索工具(Cheat Engine / GameGuardian 等)搜到地址,再用本 API 读取。
需要 root(读的是目标应用进程的内存);省略包名时读当前 App 自身进程。
地址每个进程、每次启动都不同,不要写死。
type (string, 可选,默认 'U32'): 数据类型(大小写不敏感),完整别名表:
1 字节:I8(= S8 / SBYTE / INT8 / CHAR,-128~127)、U8(= BYTE / UINT8 / BOOL / BOOLEAN,0~255)
2 字节:I16(= SHORT / INT16,-32768~32767)、U16(= WORD / USHORT / UINT16,0~65535)
4 字节:I32(= INT / INT32,-2147483648~2147483647)、U32(= DWORD / UINT32,0~4294967295)
8 字节:I64(= LONG / INT64)、U64(= QWORD / ULONG / UINT64,读指针用这个)
4 字节:FLOAT(= F32 / SINGLE)、8 字节:DOUBLE(= F64)
变长:STRING(= STR / UTF8 / UTF-8 / TEXT)按 UTF-8 解码、遇到 0 结束符截断
变长:HEX(= BYTES / RAW)返回 "F4 01 00 00" 这种空格分隔的大写十六进制
常用对应:C 的 int / 血量 / 金币 → I32 / U32;C 的 float / 坐标 → FLOAT;C 的 double → DOUBLE;指针 → U32(32 位进程)/ U64(64 位进程)。
packageName (string, 可选): 目标应用包名。省略时沿用上一次解析到的目标进程(方便连续读同一个游戏),连上一次也没有则用当前 App 自身进程。
要切换 / 明确目标就显式传:MemoryRead(addr, "I32", "com.example.game")
length (number, 可选,默认 64): 仅 STRING / HEX 生效:读取字节数,最大 4096(超过按 4096 处理,传 0 或负数按默认 64)。
STRING 给 32 通常就够读角色名;HEX 建议用 16 的倍数,方便和内存工具里的一行对齐。
I64 / U64 超过 2^53(约 9×10¹⁵)会丢精度,只建议用来读指针数值或做比较(比较刚读出的两个地址是否相等没问题)。
string: STRING 返回文本(空字符串也算成功,说明首字节就是 0);HEX 返回大写十六进制串,如 "F4 01 00 00"。
nil: 读取失败(失败原因同时写进脚本日志),常见原因:
· 地址没映射 / 跨到未映射页(偏移算错、对象还没创建、对象已释放)
· 只读到 N 字节,不足 M 字节(地址在页尾,剩余字节不可读)
· 目标进程已退出 / 包名不对 / 没有 root 或 su 授权被拒
· 类型名写错(可用类型见参数表)
-- 准备:先拿基址(下面所有偏移都是编造的示例值,请换成你自己分析出来的)
local base = GetModuleHandle("com.example.game", "libunity.so")
if base == nil then
log("先解决基址问题(见 GetModuleHandle 的示例)")
return
end
-- ===== 1) 四种基础读法 =====
log("血量(I32) = " .. tostring(MemoryRead(base + 0x1A2B38, "I32")))
log("金币(U32) = " .. tostring(MemoryRead(base + 0x1A2B3C, "U32")))
log("坐标X(FLOAT)= " .. tostring(MemoryRead(base + 0x1A2B40, "FLOAT")))
log("倍率(DOUBLE)= " .. tostring(MemoryRead(base + 0x1A2B48, "DOUBLE")))
-- ===== 2) 同一个地址用不同类型读,理解「类型 / 字节数」 =====
local addr = base + 0x2000
log("HEX = " .. tostring(MemoryRead(addr, "HEX", nil, 8))) -- 先看原始字节,例如 F4 01 00 00 00 00 00 00
log("U8 = " .. tostring(MemoryRead(addr, "U8"))) -- 只看第 1 个字节 → 244
log("U16 = " .. tostring(MemoryRead(addr, "U16"))) -- 前 2 字节 → 500
log("U32 = " .. tostring(MemoryRead(addr, "U32"))) -- 前 4 字节 → 500
log("FLOAT = " .. tostring(MemoryRead(addr, "FLOAT"))) -- 同样 4 字节按 float 解释 → 约 7e-43(一眼就是类型不对)
-- 结论:读出来的数值「离谱」,多半是类型挑错了;用 HEX 对一下就知道真实存放格式
-- ===== 3) 有符号与无符号的差别(同一块内存,两种读法) =====
-- 内存里 4 个字节是 FF FF FF FF 时:
-- I32 读 → -1
-- U32 读 → 4294967295
-- 内存里 1 个字节是 F4 时: I8 读 → -12,U8 读 → 244
-- ===== 4) 读字符串(默认 64 字节,遇到 0 结束符截断) =====
log("角色名 = " .. tostring(MemoryRead(base + 0x2100, "STRING"))) -- 默认读 64 字节
log("公会名 = " .. tostring(MemoryRead(base + 0x2140, "STRING", nil, 32))) -- 只读 32 字节,更快也更安全
-- 中文没问题(按 UTF-8 解码);读到的比预期短,通常是原数据里本来就有 0 字节
-- ===== 5) 读原始字节,和内存工具对照 =====
log(tostring(MemoryRead(base + 0x2000, "HEX", nil, 16))) -- 16 字节 = 内存视图的一行
log(tostring(MemoryRead(base + 0x2000, "HEX", nil, 64))) -- 64 字节,看整体结构
-- ===== 6) 多级指针(实战里最常用) =====
local function readPtr(a) return MemoryRead(a, "U64") end -- ARM64 指针 8 字节
local p1 = readPtr(base + 0x30) -- 第一级
if p1 then
local p2 = readPtr(p1 + 0x18) -- 第二级
if p2 then
log("玩家血量 = " .. tostring(MemoryRead(p2 + 0x10, "I32")))
log("玩家名字 = " .. tostring(MemoryRead(p2 + 0x20, "STRING", nil, 32)))
else
log("第二级指针为空:对象还没创建(回到游戏里再试)")
end
end
-- ===== 7) 读连续数组:按步长循环 =====
-- 例:背包 10 个格子的物品 ID(U32,相邻元素间隔 4 字节)
local bagAddr = base + 0x3000
local ids = {}
for i = 0, 9 do
ids[#ids + 1] = tostring(MemoryRead(bagAddr + i * 4, "U32"))
end
log("背包物品ID = " .. table.concat(ids, ", "))
-- ===== 8) 读结构体数组:元素大小 + 字段偏移 =====
-- 例:每个怪物 0x50 字节,血量在元素内偏移 4,等级在偏移 0x0C
for i = 0, 2 do
local e = base + 0x4000 + i * 0x50
log(string.format("怪物%d:血量=%s 等级=%s", i + 1,
tostring(MemoryRead(e + 4, "I32")), tostring(MemoryRead(e + 0x0C, "I32"))))
end
-- ===== 9) 读坐标(3 个连续 float:x, y, z) =====
local x = MemoryRead(base + 0x1A2B50, "FLOAT")
local y = MemoryRead(base + 0x1A2B54, "FLOAT")
local z = MemoryRead(base + 0x1A2B58, "FLOAT")
log(string.format("坐标 = (%.2f, %.2f, %.2f)", x, y, z))
-- ===== 10) 读失败给默认值,避免脚本被 nil 打断 =====
local function readOr(addr, t, def)
local v = MemoryRead(addr, t)
if v == nil then return def end
return v
end
log("金币(读失败就按 0 显示)= " .. tostring(readOr(base + 0x1A2B3C, "U32", 0)))
-- 注意:nil 参与算术会报错(attempt to perform arithmetic on a nil value),拿到值先判断再用
-- ===== 11) 轮询监控数值变化(1 秒一次,共 10 次) =====
local last = nil
for i = 1, 10 do
local hp = MemoryRead(base + 0x1A2B38, "I32")
if hp ~= last then
log(string.format("[%d] 血量变化:%s → %s", i, tostring(last), tostring(hp)))
last = hp
end
sleep(1000)
end
-- ===== 12) 省略包名 = 沿用上一次目标;显式传包名 = 固定目标 =====
local selfBase = GetModuleHandle("libc.so") -- 只传一个参数 → 当前 App 自身进程
if selfBase then
log("自身进程 ELF 魔数 = " .. tostring(MemoryRead(selfBase, "HEX", nil, 4))) -- 7F 45 4C 46
end
log("指定包名读同一个地址 = " .. tostring(MemoryRead(base + 0x1A2B3C, "U32", "com.example.game")))
-- ===== 13) 读不到?按顺序排查 =====
local v = MemoryRead(base + 0x1A2B3C, "I32")
if v == nil then
log("① 偏移是不是这个游戏版本的(更新后偏移会变)")
log("② 目标进程还活着吗:" .. tostring(execRoot("pidof com.example.game")))
log("③ 换成 HEX 读同一地址:" .. tostring(MemoryRead(base + 0x1A2B3C, "HEX", nil, 16)))
log("④ 是否已 root 并授权 su;对象是否还没创建")
end支持整数 / 浮点 / 字符串 / 原始字节(HEX)四种写法;整数按类型的字节数截断(写 I32 就是低 4 字节),不做范围校验,所以类型一定要写对。
常见用途:修改血量、金币、坐标等「每次读取都直接来自内存」的数值。
重要提醒:
① 先读后写:先 MemoryRead 记下原值,再写,写完再读一次验证(也方便恢复)。
② 很多数值由服务器或每帧逻辑回写,改完立刻被覆盖 —— 这说明它不是数据源头,需要顺着指针链往上游找(见 GetModuleHandle / MemoryRead 示例)。
③ 写 STRING 不会自动补 0 结束符:新值比原值短时会残留旧尾巴;要精确控制请用 HEX(例如写 "31323300")。
④ 只读页 / 代码段 / 已释放的地址:dd 会失败或回读不一致,本 API 返回 false 并把原因写进日志。
需要 root(写的是别的进程内存);省略包名时写当前 App 自身进程(不建议随手改自身进程内存,容易崩溃)。
value (number/string): 要写入的值,按 type 解释:
整数类型(I8/U8/I16/U16/I32/U32/I64/U64):数字 500、-1;也可写十六进制字符串 "0x1F4"(= 500)或 "500.0" 这种写法
浮点(FLOAT / DOUBLE):数字 1.5、8888.5,或数字字符串 "8888.5"
STRING:文本(按 UTF-8 编码,不会自动补 0 结束符)
HEX:十六进制字节串,支持 "9090" / "90 90" / "0x9090" / "90,90";也可直接写数字,按 4 字节小端写出(500 → F4 01 00 00)
type (string, 可选,默认 'U32'): 数据类型,类型表与 MemoryRead 完全相同(含所有别名;STRING 写文本、HEX 写原始字节)。
packageName (string, 可选): 目标应用包名。省略时沿用上一次解析到的目标进程,连上一次也没有则写当前 App 自身进程。
要切换 / 明确目标就显式传:MemoryWrite(addr, 99999, "I32", "com.example.game")
· 十六进制串格式问题(长度不是偶数 / 含非法字符 / 为空)
· 值没法解析成整数或浮点数(比如写 "abc")
· 类型名写错
· dd 返回非 0:地址所在页只读 / 越界 / 目标进程已退出 / 没有 root 或 su 授权被拒
· 写入后回读不一致:被目标进程覆盖(游戏每帧刷新数值)
-- 推荐模式:先读 → 判断 → 写 → 回读验证
local base = GetModuleHandle("com.example.game", "libunity.so")
if base == nil then
log("先拿到基址再写(见 GetModuleHandle 示例)")
return
end
local HP = base + 0x1A2B38
-- ===== 1) 标准写法:读 → 写 → 验证 =====
local old = MemoryRead(HP, "I32")
log("原血量 = " .. tostring(old))
if old == nil then
log("连读都读不到,先排查地址 / 权限,别急着写")
return
end
if MemoryWrite(HP, 99999, "I32") then
log("写入成功,现在血量 = " .. tostring(MemoryRead(HP, "I32"))) -- 99999
else
log("写入失败(原因见日志上一行)")
end
-- ===== 2) 各种类型怎么写 =====
MemoryWrite(base + 0x1A2B38, 1000, "I32") -- 整数
MemoryWrite(base + 0x1A2B3C, -1, "I32") -- 有符号负数(内存里是 FF FF FF FF)
MemoryWrite(base + 0x1A2B40, 0.5, "FLOAT") -- 浮点
MemoryWrite(base + 0x1A2B48, 1.25, "DOUBLE") -- 双精度
MemoryWrite(base + 0x1A2B50, "0x1F4", "U32") -- 十六进制字符串写法:0x1F4 = 500
MemoryWrite(base + 0x1A2B54, "8888.5", "FLOAT") -- 数字字符串也行
MemoryWrite(base + 0x1A2B60, "古龙", "STRING") -- UTF-8 文本(不自动补 0,别比原值长)
MemoryWrite(base + 0x1A2B70, "90909090", "HEX") -- 4 个 NOP 字节
MemoryWrite(base + 0x1A2B74, "00 00 00 00", "HEX") -- 用空格分隔的写法更直观
MemoryWrite(base + 0x1A2B78, "0x1F4", "HEX") -- HEX 写数字:按 4 字节小端写出
-- ===== 3) 写坐标(3 个连续 float,步长 4) =====
local function writePos(addr, x, y, z)
if not MemoryWrite(addr, x, "FLOAT") then return false end
if not MemoryWrite(addr + 4, y, "FLOAT") then return false end
return MemoryWrite(addr + 8, z, "FLOAT")
end
if writePos(base + 0x1A2B50, 100.5, 200.0, 0) then
log("坐标已写入")
end
-- ===== 4) 批量写入:连续数组 =====
local bagAddr = base + 0x3000
local okCount = 0
for i = 0, 9 do
if MemoryWrite(bagAddr + i * 4, 1000 + i, "U32") then
okCount = okCount + 1
else
log("第 " .. (i + 1) .. " 个格子写入失败:" .. string.format("0x%X", bagAddr + i * 4))
end
end
log(string.format("背包写入成功 %d/10", okCount))
-- ===== 5) 写完立刻看内存原貌,确认字节序 =====
MemoryWrite(base + 0x1A2B3C, 500, "U32")
log("500 写进去后内存 = " .. tostring(MemoryRead(base + 0x1A2B3C, "HEX", nil, 4))) -- F4 01 00 00
-- ===== 6) 备份 → 临时修改 → 恢复(练习时推荐) =====
local backup = MemoryRead(HP, "I32")
if backup ~= nil then
MemoryWrite(HP, 1, "I32") -- 临时改成 1
log("已临时修改,3 秒后恢复…")
sleep(3000)
MemoryWrite(HP, backup, "I32")
log("已恢复为原值 " .. tostring(MemoryRead(HP, "I32")))
end
-- ===== 7) 封装:写失败自动重试(对象还没创建时特别有用) =====
local function writeRetry(addr, v, t, times)
times = times or 5
for i = 1, times do
if MemoryWrite(addr, v, t) then return true end
log("第 " .. i .. " 次写入失败,1 秒后重试…")
sleep(1000)
end
return false
end
if not writeRetry(base + 0x1A2B38, 50000, "I32") then
log("多次都失败:地址可能只读 / 已失效(对象被销毁、游戏版本变了)")
end
-- ===== 8) 一次性把结果写进日志,便于观察 =====
local function tryWrite(addr, v, t)
local before = MemoryRead(addr, t)
local ok = MemoryWrite(addr, v, t)
local after = MemoryRead(addr, t)
log(string.format("写 %s → %s :%s(%s → %s)", t, tostring(v), ok and "成功" or "失败",
tostring(before), tostring(after)))
return ok
end
tryWrite(base + 0x1A2B3C, 12345, "U32")
-- ===== 9) 写了「没用」?按这几种情况排查 =====
-- ① 值被游戏持续回写:改成循环里连续写几次验证
local loopAddr = base + 0x1A2B38
local writes = 0
for i = 1, 10 do
if MemoryWrite(loopAddr, 9999, "I32") then writes = writes + 1 end
sleep(200)
end
log("连续写入成功 " .. writes .. " 次;若界面数值仍不动,说明这不是生效的字段")
-- ② 类型写错:float 字段用 I32 写进去,读出来是「乱码」数值 → 用 HEX 对照原内存
-- ③ 地址在只读页(.rodata / 代码段):找存放该值的可写副本,或改指针链上游
-- ④ 游戏更新后偏移变化 / 基址变化:脚本每次运行都要重新取基址、重新核对偏移♿ 无障碍方法 10个API
| 函数名 | 说明 | 示例 |
|---|---|---|
| getCaptureMode | 获取当前截图模式(root / mediaprojection / unknown) | local mode = getCaptureMode() |
| setFindColorDelay | 设置无障碍模式下找到颜色后的等待延时(毫秒),等待屏幕稳定 | setFindColorDelay(200) |
🌳 节点操作 nodeLib(需要无障碍服务)
用无障碍树直接找节点、点节点,比「找图 / 找色」更稳、更快:一次遍历生成整棵界面快照(纯 Java 数据,300 ms 内连续查询复用),之后 findOne / findAll / findParentNode 都在快照上匹配,不反复走无障碍 IPC。快照最多 4000 个节点 / 60 层,超出自动截断,避免超大页面卡死脚本;click 之后缓存自动失效。
⚠️ 节点 API 依赖「古龙精灵」无障碍服务(系统设置 → 无障碍 → 古龙精灵)。未开启时查询返回 nil / 空表,日志会提示原因。
-- ① 检查无障碍是否连上:refresh() 返回当前界面节点总数,-1 = 取不到窗口 local total = nodeLib.refresh() if total <= 0 then toast("请先开启「古龙精灵」无障碍服务") return end log("当前界面共 " .. total .. " 个节点") -- ② 等按钮出现:最多 15 秒,第 3 个参数 true = 模糊匹配("开始" 也能命中「点击开始」) local startBtn = nodeLib.waitFor({text="开始游戏", clickable=true}, 15000, true) if startBtn == nil then log("15 秒没等到按钮:确认游戏在前台,或用 nodeLib.getXml() 看真实文本") return end -- ③ 点击:返回 true 只表示「动作已发给系统」,不代表页面一定跳转 if not nodeLib.click(startBtn) then log("点击失败:无障碍被系统回收,或节点已经消失") return end -- ④ 等新页面:用「新页面独有」的节点判断,不要去等刚才那个按钮消失 local bag = nodeLib.waitFor({text="背包"}, 10000, true) if bag ~= nil then log("已进入主界面,背包入口中心点: " .. bag.cx .. "," .. bag.cy) else log("点击后 10 秒没看到主界面:可能弹了公告,先跑下面「一键关弹窗」配方") end
🧭 我该用哪个函数?
| 你要做的事 | 用这个 | 为什么 |
|---|---|---|
| 找一个节点,找不到就跳过 | nodeLib.findOne | 返回节点表或 nil,日常 90% 的查找都用它 |
| 把同类节点全找出来(列表、一堆「使用」按钮) | nodeLib.findAll | 返回数组 + n,可 #list 取数量,找不到返回空表 |
| 等它出现(加载完、弹窗弹出、按钮亮起) | nodeLib.waitFor | 内部每 100 ms 重抓一次,支持暂停 / 停止,比手写循环短且不会卡死 |
| 点一下 / 长按 | nodeLib.click | 先无障碍点击,再向上找可点祖先,最后坐标兜底,成功率最高 |
| 点文字所在的那一整行 / 整个卡片 | nodeLib.findParentNode | 先按文字找到节点,再返回父节点;点整行比点文本稳 |
| 已经知道节点在第几层第几个(日志里见过 path) | nodeLib.findByIndex | 只沿路径下钻,不遍历整棵树,最快 |
| 界面刚变,马上要查最新结构 | nodeLib.refresh | 丢弃 300 ms 缓存并重建快照;同时可用返回值判断无障碍是否可用 |
| 不知道页面上有什么,要写选择器 | nodeLib.getXml | 导出整棵树的 XML,看真实 class / id / text |
🎯 节点操作 vs 找图 / 找色
| 界面类型 | 推荐方案 | 原因 |
|---|---|---|
| 原生界面:登录、公告弹窗、菜单、背包列表、奖励结算 | 节点操作 | 不看分辨率、不怕换皮换色,文字改了也能用模糊匹配;速度是毫秒级 |
| 游戏内画面:Unity / Cocos 的按钮、怪物、地图 | 找图 / 找色 / OCR | 这类界面用 SurfaceView 直接绘制,无障碍树里往往只有一个大节点,看不到里面的按钮 |
| 混合界面(原生外壳 + 游戏画布) | 先用节点找外壳按钮,画布内用找图 | 两种方式可以混用,节点找到坐标后再用 tap(node.cx, node.cy) 也完全可以 |
| 选择器字段(selector) | 说明 |
|---|---|
| class / package | 完整类名与包名,如 android.widget.TextView(完全相等或模糊匹配) |
| id | 控件 id。可只写斜杠后面的名字(id="btn_ok" 能匹配 com.xxx:id/btn_ok) |
| text / desc / hint | 文本、contentDescription、输入框提示。模糊匹配时支持 *(任意长度)与 ?(单个字符),全角 * ? 同样有效;没有通配符则按「包含」匹配 |
| clickable / long_clickable / editable / scrollable / checkable / checked / selected / enabled / focused / visible / password | 布尔条件,例如 clickable=true 只找可点击节点 |
| min_depth / max_depth | 只看指定深度的节点(根节点 depth=0) |
| 空字符串 | 值为 "" 的字段按「未填写」处理,不参与匹配(节点工具勾选出来的 id="" / package="" 可以直接用) |
| 简写 | selector 也可以直接传字符串,等价于 {text=字符串},如 nodeLib.findOne("开始游戏") |
| content_desc / contentDescription | desc 的别名,从 XML 里直接抄属性名也能用 |
| minDepth / maxDepth | min_depth / max_depth 的别名,两种写法都认 |
| 节点表字段(返回值) | 说明 |
|---|---|
| text / desc / id / class / package / hint | 节点原始属性(缺失时为空字符串) |
| label | 给人看的名字:text > desc > id > class |
| path | 索引路径(如 "0|1|2"),可直接回传给 nodeLib.click / nodeLib.findByIndex |
| depth / index / children | 层级、在父节点中的序号、子节点数量 |
| n(仅 findAll 返回的数组上有) | 数组长度,等价于 #list;一个都没找到时是 0 |
| left / top / right / bottom / cx / cy / w / h / bounds | 屏幕坐标与尺寸,bounds 为 {left,top,right,bottom,cx,cy} |
| clickable / long_clickable / editable / scrollable / checkable / checked / selected / enabled / focused / visible / password | 节点状态(布尔) |
"0|1|2" 路径)。点击顺序(古龙增强,点到的概率更高):① 无障碍
ACTION_CLICK → ② 节点本身不可点时自动向上找最多 10 层可点击祖先 → ③ 还不行就按 bounds 中心点用坐标点击兜底(tap)。第 2 个参数传毫秒数可长按(先 ACTION_LONG_CLICK,失败再 longTap)。点不到返回 false 并写中文日志(无障碍没开 / 节点已失效 / 手势被系统取消)。注意:返回 true 只代表「动作已经发给系统」,不代表界面一定跳转 —— 点完要用
nodeLib.waitFor 等新界面,或再查一次节点来确认结果。-- 场景 1:最普通的点击(先判空 → 点 → 看返回值) local node = nodeLib.findOne({class="android.widget.Button", text="确定"}) if node ~= nil then if not nodeLib.click(node) then log("点击失败:无障碍没连上,或节点已经消失") end end -- 场景 2:点之前先确认「看得见 + 没被禁用」,避免点到隐藏节点 local btn = nodeLib.findOne({text="领取", clickable=true}, true) if btn ~= nil and btn.visible and btn.enabled then nodeLib.click(btn) else log("领取按钮不可见或被禁用,奖励可能还没到账") end -- 场景 3:无障碍点不动时的最后兜底 —— 用节点中心点坐标点击 local close = nodeLib.findOne({desc="关闭"}, true) if close ~= nil and not nodeLib.click(close) then log("节点点击失败,改用坐标: " .. close.cx .. "," .. close.cy) tap(close.cx, close.cy) end -- 场景 4:长按(删除、拖动、查看详情)—— 第 2 个参数是毫秒数 local item = nodeLib.findOne({text="强化石"}, true) if item ~= nil then nodeLib.click(item, 800) -- 先 ACTION_LONG_CLICK,失败自动 longTap end -- 场景 5:已经知道路径时直接点(最快,省一次查找;结构变了会失败) nodeLib.click("0|1|2")
nil。选择器字段见上方表格,可任意组合(全部满足才算命中)。fuzzy_match 说明:true = 字符串支持
* / ? 通配符;没有通配符时按「包含」匹配(text="确定" 能匹配「点击确定」),且 id 忽略包名前缀。false(默认)= 完全相等。字段别堆太多:所有条件是「与」关系,写多了容易一个都匹配不上。建议先用一个最能区分的关键字段(text 或 id)确认能找到,再逐步加
clickable / class 收窄。性能:整个查询只遍历一遍界面树;最多 4000 节点 / 60 层;同一帧内的连续查询复用快照。
找不到时:先看下面「🩺 找不到 / 点不到」的排查清单,不要靠猜改文本。
-- 写法 1:精确匹配(节点工具勾选出来的写法,id / class 一字不差) local n1 = nodeLib.findOne({class="android.widget.TextView", text="名称: tmp.lr"}) -- 写法 2:模糊匹配 + 可点击(文本「包含」确定,且节点本身可点) local n2 = nodeLib.findOne({text="确定", clickable=true}, true) -- 写法 3:模糊 + 通配符(id 可省略包名,btn_* 能匹配 com.xxx:id/btn_ok) local n3 = nodeLib.findOne({id="btn_*"}, true) -- 写法 4:简写(等价于 {text="开始游戏"},完全相等,不带模糊) local n4 = nodeLib.findOne("开始游戏") if n4 ~= nil then log("找到: " .. n4.label .. " 中心点 " .. n4.cx .. "," .. n4.cy) end -- 写法 5:文字为空时,用 contentDescription(desc)找图标按钮 local close = nodeLib.findOne({desc="关闭"}, true) -- 写法 6:用输入框提示(hint)定位输入框,比 class 更准 local edit = nodeLib.findOne({editable=true, hint="请输入"}, true) -- 写法 7:同名节点太多?加深度区间,避开列表里重复的标题 local title = nodeLib.findOne({class="android.widget.TextView", text="背包", min_depth=2, max_depth=6}) -- 写法 8:找不到时别靠猜 —— 把可点击节点的 label 全打出来,看真实文本长什么样 if nodeLib.findOne({text="领取"}) == nil then local all = nodeLib.findAll({clickable=true}) for i = 1, #all do log("候选 " .. i .. ": [" .. all[i].label .. "] class=" .. all[i].class) end end
#list 或 list.n 取数量。每个元素都是节点表(字段同上),节点表是纯数据快照,可以存起来、打印、写文件;真正点击时
nodeLib.click 会按 path 在实时树里重新定位,界面变了会按 text/id/class + 屏幕距离找回。#list / list.n 为数量;找不到时为空表-- ① 找出所有可点击按钮,打印数量与位置 local list = nodeLib.findAll({class="android.widget.Button", clickable=true}) log("共找到 " .. list.n .. " 个按钮") for i = 1, #list do local node = list[i] log(i .. ": " .. node.label .. " @ " .. node.cx .. "," .. node.cy) end -- ② 按屏幕位置排序(列表控件返回顺序不一定是视觉顺序时很有用) table.sort(list, function(a, b) if a.top ~= b.top then return a.top < b.top end return a.left < b.left end) -- ③ 只要某一个:第 3 个 / 随机一个(「随机点一个商品」很常用) if #list >= 3 then nodeLib.click(list[3]) end if #list > 0 then nodeLib.click(list[math.random(1, #list)]) end -- ④ 模糊匹配后逐个点击:点完界面可能重排,所以每次先 refresh 再按文本重找 local tasks = nodeLib.findAll({text="任务"}, true) for i = 1, #tasks do nodeLib.refresh() local again = nodeLib.findOne({text=tasks[i].label}, true) if again ~= nil then nodeLib.click(again) sleep(300) end end -- ⑤ 没找到时是空表(不是 nil):直接用 # 判断,不会报错 local empty = nodeLib.findAll({text="绝不存在的文字"}) log("数量: " .. #empty) -- 输出 0
| 分隔,根节点固定为 0。例如 "0|1|2" = 根节点 → 第 1 个子节点 → 第 2 个子节点;也可以传数组 {0,1,2}。path 从哪来:任何节点表的
path 字段(用 findOne 找到一个节点后 log(node.path) 即可看到);去掉 path 最后一段就是父节点路径,所以可以用它向上取任意层祖先。性能:不遍历整棵树,只沿路径逐层
getChild 下钻,复杂度 O(路径长度),已知位置时最快;界面变化导致路径失效时返回 nil 并写日志 —— 所以务必判空,并用 findOne 兜底。-- 字符串路径:根 → 第 1 个孩子 → 第 2 个孩子(根固定为 0) local node = nodeLib.findByIndex("0|1|2") if node ~= nil then log("找到: " .. node.label .. " 路径 " .. node.path) nodeLib.click(node) end -- 数组路径(完全等价) local same = nodeLib.findByIndex({0, 1, 2}) -- 路径从哪来:findOne 找到后打印 node.path,下次就能直接下钻(最快) local target = nodeLib.findOne({text="开始游戏"}) if target ~= nil then log("这个按钮的路径是: " .. target.path) -- 技巧:截掉 path 最后一段 = 父节点路径,可以一次向上取好几层 local function parentPath(path) return string.match(path, "^(.*)|%d+$") end local p = parentPath(target.path) if p ~= nil then local row = nodeLib.findByIndex(p) -- 父节点(通常就是整行) if row ~= nil then nodeLib.click(row) end end end -- 页面结构会变:路径失效时返回 nil,必须准备兜底方案 local fast = nodeLib.findByIndex("0|1|3|0") if fast == nil then log("路径失效(页面结构变了),改用 findOne 重新找") fast = nodeLib.findOne({text="开始游戏"}, true) end if fast ~= nil then nodeLib.click(fast) end
nodeLib.findOne 完全一致。为什么点父节点:很多界面的文字(TextView)本身不可点,真正接收点击的是外面那层 Cell / 卡片,点文本会「点了个寂寞」。
找到的节点已经是根节点、或没找到匹配节点时返回 nil;父节点表的
path 已是父节点路径,可直接交给 nodeLib.click。想一次向上取多层,用 path 逐段截断更可靠(见示例 ③)。-- ① 点文字所在整行:文字本身通常不可点,父节点才是真正接收点击的区域 local row = nodeLib.findParentNode({text="银月贤者"}, true) if row ~= nil then log("整行: " .. row.class .. " " .. row.bounds.left .. "," .. row.bounds.top) nodeLib.click(row) else log("没有找到「银月贤者」") end -- ② 背包格子:按格子里的物品名,点到整个格子(格子可点,名字不可点) local cell = nodeLib.findParentNode({text="强化石"}, true) if cell ~= nil and cell.clickable then nodeLib.click(cell) end -- ③ 父节点也不可点?用 path 一层层往上爬(最多 3 层),谁可点就点谁 local node = nodeLib.findOne({text="每日任务"}, true) if node ~= nil then local path = node.path for i = 1, 3 do path = string.match(path, "^(.*)|%d+$") -- 去掉最后一段 = 父节点 if path == nil then break end -- 已经是根节点 local up = nodeLib.findByIndex(path) if up == nil then break end log("向上第 " .. i .. " 层: " .. up.class .. " clickable=" .. tostring(up.clickable)) if up.clickable then nodeLib.click(up) break end end end
<node> 带 index、text、resource-id、class、package、content-desc、clickable、long-clickable、checkable、checked、selected、enabled、focused、scrollable、editable、password、visible-to-user、bounds 属性。什么时候用:写选择器之前、日志提示「没找到」时、怀疑页面是 WebView / 游戏画布时。怎么判断:如果 XML 里只有一两个铺满全屏的节点、没有任何文字,说明这是 SurfaceView / Unity 画布,节点方案拿不到里面的按钮,请改用找图 / 找色 / OCR。
节点超过 4000 个或深度超过 60 层会自动截断并在末尾注释说明;内容较长,建议写到文件而不是直接打印到日志。
-- ① 导出完整 XML 到文件(内容很长,不要直接 log 整个字符串) local xml = nodeLib.getXml() if xml ~= "" then write_file("/sdcard/node_dump.xml", xml) log("节点 XML 已保存,共 " .. #xml .. " 字节") else log("取不到节点树:请先开启无障碍服务") end -- ② 只想快速看一眼结构:截前 800 个字符打印 if xml ~= "" then log(string.sub(xml, 1, 800)) end -- ③ 写选择器更快的办法:空选择器 = 全部节点,统计都有哪些控件类型 local all = nodeLib.findAll({}) local stat = {} for i = 1, #all do local c = all[i].class stat[c] = (stat[c] or 0) + 1 end for c, n in pairs(stat) do log(c .. " × " .. n) end -- ④ 相当于「界面文字大纲」:按层级缩进打印所有可见且有文字的节点 for i = 1, #all do local nd = all[i] if nd.visible and nd.text ~= "" then log(string.rep(" ", nd.depth) .. nd.text) end end
等加载、等弹窗、等按钮出现时用它,比手写循环更短、更稳。
注意:它只能等「出现」;等「消失」(loading 转圈结束、弹窗被关掉)要自己写
refresh + sleep 循环(见示例 ④)。timeout 上限 120000 ms,超时会在运行日志里写明等了多久、选择器是什么,方便定位。-- ① 等「开始游戏」出现,最多等 8 秒(true = 模糊匹配) local btn = nodeLib.waitFor({text="开始游戏", clickable=true}, 8000, true) if btn ~= nil then nodeLib.click(btn) else log("8 秒内没等到开始按钮,检查是否卡在登录页") end -- ② 开屏公告:先等公告出现(5 秒内没有就跳过),再点关闭 local notice = nodeLib.waitFor({text="公告"}, 5000, true) if notice ~= nil then local close = nodeLib.findOne({desc="关闭"}, true) if close ~= nil then nodeLib.click(close) end end -- ③ 等列表加载完:等某个列表项出现,比 sleep(3000) 稳得多 local item = nodeLib.waitFor({class="android.widget.TextView", text="第 1 名", min_depth=2}, 12000, true) log(item ~= nil and "排行榜已加载" or "12 秒还没加载出来") -- ④ 等「消失」:waitFor 做不到,自己轮询(50 次 × 200ms ≈ 10 秒) for i = 1, 50 do nodeLib.refresh() -- 轮询必须看最新界面 if nodeLib.findOne({text="加载中"}, true) == nil then log("加载完成") break end sleep(200) end
为了更快,nodeLib 的查询会在 300 ms 内复用同一份快照;如果界面刚变化、又必须立刻拿到最新结构(例如切换页面后马上查找),先调用它。
什么时候不用调:
nodeLib.click 成功后缓存会自动失效,紧接着的 findOne / findAll 已经是新界面;只有「界面不是脚本点的」(过场动画结束、倒计时自动跳转、玩家手动点过)才需要手动 refresh。-- ① 当「无障碍是否可用」的探针:返回值 <= 0 就是取不到窗口 local total = nodeLib.refresh() if total <= 0 then log("取不到节点树:无障碍没开 / 被系统回收 / 当前是受保护页面") return end log("当前界面节点数: " .. total) -- ② 非脚本触发的页面变化(过场动画、倒计时自动跳)之后,先刷新再查 sleep(1500) nodeLib.refresh() local bag = nodeLib.findOne({text="背包"}, true) if bag ~= nil then nodeLib.click(bag) end -- ③ 反例:click 之后缓存已自动失效,这里不需要再 refresh local ok = nodeLib.findOne({text="确定"}, true) if ok ~= nil then nodeLib.click(ok) -- 点击成功后自动 invalidate local next = nodeLib.findOne({text="返回"}, true) -- 直接查就是新页面 end
🍳 常见场景配方(可直接抄)
下面每段都是完整可运行的片段,改一下文本 / id 就能用。共同套路:先等 → 再判空 → 再操作 → 再校验,任何一步失败都写日志,绝不让脚本静默跳过。
-- 常见关闭文案挨个试:文字本身可点就点文字,否则点它的父节点(整行 / 整个按钮) local CLOSE_TEXTS = {"关闭", "×", "知道了", "我知道了", "确定", "取消", "跳过", "以后再说"} local function closePopup() for _, t in ipairs(CLOSE_TEXTS) do local n = nodeLib.findOne({text=t, clickable=true}, true) if n == nil then n = nodeLib.findParentNode({text=t}, true) end if n ~= nil and nodeLib.click(n) then log("已关闭弹窗: " .. t) sleep(300) return true end end return false end if closePopup() then log("弹窗处理完毕") end
-- 等出现 → 判可见 / 可用 → 点 → 返回结果;三处失败都写日志,方便定位 local function waitAndClick(selector, timeout, fuzzy) local n = nodeLib.waitFor(selector, timeout or 5000, fuzzy ~= false) if n == nil then log("等不到节点: " .. tostring(selector.text or selector.id or selector)) return false end if not n.visible or not n.enabled then log("节点不可见 / 已禁用: " .. n.label) return false end return nodeLib.click(n) end -- 用法:最多等 15 秒,点「开始游戏」(模糊匹配) if waitAndClick({text="开始游戏", clickable=true}, 15000, true) then log("已点击开始游戏") end
-- 先看当前屏能不能找到;找不到就滑动可滚动区域,划完必须 refresh 再查 local function scrollFind(text, maxTimes) for i = 1, maxTimes or 10 do local target = nodeLib.findOne({text=text}, true) if target ~= nil then return target end local list = nodeLib.findOne({scrollable=true}) if list == nil then log("当前页面没有可滚动区域") return nil end -- 从列表下部划到上部 = 向下翻页,滑动距离取列表高度的 60% local x = list.cx local y1 = list.top + math.floor(list.h * 0.8) local y2 = list.top + math.floor(list.h * 0.2) swipe(x, y1, x, y2, 400) sleep(500) nodeLib.refresh() end return nil end local item = scrollFind("限时礼包", 10) if item ~= nil then nodeLib.click(item) else log("翻了 10 次没找到「限时礼包」") end
-- 先点输入框拿焦点,再用 imeLib 写入(比 input_text 稳,支持中文、不弹系统键盘) local edit = nodeLib.waitFor({editable=true}, 5000, true) if edit ~= nil then if not nodeLib.click(edit) then log("输入框点不中,改用坐标") tap(edit.cx, edit.cy) end sleep(300) imeLib.clearText() -- 清空旧内容(无参数) imeLib.setText("古月") -- 写入文字 imeLib.finishInput() -- 回车确认(部分游戏需要) log("已输入「古月」") end -- 首次使用 imeLib:需在系统设置里启用一次「古龙输入法」(详见「输入法」章节)
-- 先看当前状态:已勾选就不要重复点(再点一次会取消勾选) local cb = nodeLib.findOne({checkable=true, text="同意"}, true) if cb == nil then log("没找到勾选框,可能外层才是 checkable 节点") cb = nodeLib.findParentNode({text="同意"}, true) end if cb ~= nil and not cb.checked then nodeLib.click(cb) -- 校验:勾选状态可能晚 1~2 帧才更新,用 waitFor 等它变成 true local ok = nodeLib.waitFor({checkable=true, text="同意", checked=true}, 2000, true) log(ok ~= nil and "已勾选" or "勾选没生效:试试点父节点,或用 tap(cb.cx, cb.cy)") end
-- 每次只点「最上面那个」,点完立刻刷新;最多点 20 次,防止奖励无限弹 / 卡死 local got = 0 for i = 1, 20 do nodeLib.refresh() local list = nodeLib.findAll({text="领取", clickable=true}, true) if #list == 0 then break end -- 从上到下点,视觉上更自然,也避免总点同一个位置 table.sort(list, function(a, b) return a.top < b.top end) if not nodeLib.click(list[1]) then log("点击失败,停止批量领取") break end got = got + 1 sleep(400) end log("共领取 " .. got .. " 次")
-- 点一次不中很常见:每次先等 3 秒 → 点 → 失败睡 500ms 再来,最多 3 轮 local function clickWithRetry(selector, times, fuzzy) for i = 1, times or 3 do local n = nodeLib.waitFor(selector, 3000, fuzzy ~= false) if n ~= nil and nodeLib.click(n) then return true end log("第 " .. i .. " 次没点中,重试") sleep(500) end return false end if not clickWithRetry({text="领取", clickable=true}, 3, true) then log("领取失败,跳过这一步") end
🩺 找不到 / 点不到?按这个顺序排查
- 先确认服务可用:
local n = nodeLib.refresh(),返回 -1 或 0 → 无障碍没开 / 被系统回收 / 当前是受保护页面(支付、密码、输入法窗口),到系统设置里重新开启。 - 再确认节点真的存在:用
nodeLib.findAll({})打印界面文字大纲(见 getXml 示例 ④),看真实文本是「领取」还是「领取 」/「点击领取」,或者根本在desc里。 - 文本对但还是找不到:要么节点在屏幕外(先滑动,配方 ③),要么这个界面是 WebView / 游戏画布(节点树里只有一两个大节点),后者请改用找图 / 找色 / OCR。
- 找到了但点了没反应:换成
nodeLib.findParentNode点父级(很多文字节点不可点),或用tap(node.cx, node.cy)直接坐标点击兜底。 - 实在查不出来:
write_file("/sdcard/node_dump.xml", nodeLib.getXml()),对照 XML 里的 class / resource-id / bounds 重写选择器,比反复试文本快得多。
| 现象 | 最可能的原因 | 怎么改 |
|---|---|---|
refresh() 返回 -1 | 无障碍服务没连上、被系统回收,或当前是受保护页面 | 到系统设置里重开无障碍;脚本开头用返回值做一次判断 |
refresh() 正常,但 findOne 一直 nil | 文本在 desc / hint 里;或带空格、换行、前后缀 | 改用 {desc="关闭"};或加模糊匹配 true,用 *关闭* |
| 同名节点很多,总是点错 | findOne 只返回第一个命中的 | 加 clickable / id / min_depth 收窄;或用 findAll 按位置取 |
| 明明在列表里,就是找不到 | 目标在屏幕外,无障碍树只包含可见区域 | 滑动后再查(配方 ③),每次滑完 refresh() |
click 返回 true 但界面不变 | 点到了不可点的文字节点,或上方有透明遮罩 | 点父节点 / 点整行;再不行用 tap(cx, cy) |
click 返回 false | 服务断开、节点已消失、手势被系统取消 | 看运行日志里的中文原因;先 refresh() 再重找一次 |
| XML 只有一两个铺满全屏的节点 | SurfaceView / Unity / Cocos 画布,无障碍树里没有内部元素 | 节点方案无效,改用找图 / 找色 / OCR(见对应章节) |
| 脚本越跑越慢 | 在大循环里反复 findOne / getXml,每次都重建快照 | 一次 findAll 拿数组再处理;getXml 只在调试时用 |
💡 古龙特色小结:① 快照 + 短缓存,连续查找不再反复走无障碍 IPC;② 通配符 * ?(全角也行)+ 无通配符按包含,中文文本更好找;③ id 可省略包名前缀;④ 点击失败自动向上找可点击祖先、再降级坐标点击;⑤ 节点表是纯数据,可保存、可打印、可写文件;⑥ 找不到 / 无障碍未开 / 点击降级都会在运行日志里说明原因。
🖥️ 全分辨率方法 12个API
在 720×1280 下开发,自动适配其他分辨率。末尾参数 >255 为目标分辨率:仅当设备分辨率匹配时才执行且不做缩放;≤255 为容差(部分函数为「容差 + 相似度比例」)。
| 函数名 | 说明 | 示例 |
|---|---|---|
| setDesignResolution | 设置脚本设计分辨率,用于后续坐标/区域缩放适配 | setDesignResolution(720, 1280) |
| setAdaptTolerance | 设置分辨率缩放时的颜色适配容差 | setAdaptTolerance(20) |
| setAdaptSimScale | 设置分辨率缩放时的相似度比例 | setAdaptSimScale(0.7) |
| tap | 点击(支持指定目标分辨率) | tap(716, 357, 2000, 1200) |
| find_color | 单点/多颜色找色(支持指定分辨率) | find_color(100,200,300,250,"ff0000",10) |
| findMultiColor | 多点取色(支持指定分辨率) | findMultiColor(0,0,1080,1920,"ff0000","10|0|00ff00",0,0.9,2000,1200) |
| findMultiColorTargets | 多目标多点取色(支持指定分辨率) | findMultiColorTargets(0,0,1080,1920,MultiColorTargets,0,0.9,2000,1200) |
| findMultiColorStr | 带文字标识的多点取色(支持指定分辨率) | findMultiColorStr(0,0,1080,1920,MultiColorStr,0,0.9,2000,1200) |
| getMultiColorStr | 获取多点标记(支持指定分辨率) | getMultiColorStr(0,0,1080,1920,MultiColorStr,0,0.9,2000,1200) |
| findMultiColorAll | 查找所有匹配位置(支持指定分辨率) | findMultiColorAll(0,0,1080,1920,"ff0000","10|0|00ff00",0,0.9,2000,1200) |
| cmpColorEx | 比较多点颜色(支持指定分辨率) | cmpColorEx("ff0000","10|0|00ff00",0,0.9,2000,1200) |
| findColor | 多颜色列表找色(支持指定分辨率) | findColor(100,200,300,250,"ff0000|00ff00",0,0.9,2000,1200) |
📝 字符串方法 22个API
| 函数名 | 说明 | 示例 |
|---|---|---|
| splitStr | 根据分隔符拆分字符串(分隔符按普通文本处理,不是正则/模式;+ . * | ( ) [ ] { } ^ $ ? \ 等特殊字符可直接用,支持多字符分隔符,保留空段) | splitStr("a#b#c", "#") → {"a","b","c"} |
| string.lower | 转换为小写 | string.lower("AbC") → "abc" |
| string.upper | 转换为大写 | string.upper("AbC") → "ABC" |
| string.find | 查找子串位置 | string.find("Hello Lua", "Lua") → 7, 9 |
| string.match | 模式匹配提取内容(有 () 捕获时返回括号里的内容,多个捕获按顺序返回多个值;中文按字符匹配,取中文用 .- 更稳) | string.match("乙数比甲数多九", "多(.-)$") → "九" |
| string.gsub | 模式替换(返回「新字符串, 替换次数」两个值;替换中文与全角标点安全) | string.gsub("a1b22", "(%d+)", "[%1]") → "a[1]b[22]", 2 |
| string.reverse | 反转字符串 | string.reverse("abc") → "cba" |
| string.format | 格式化字符串 | string.format("值=%d", 666) |
| string.char / string.byte | ASCII与字符互转 | string.char(97) → "a" / string.byte("a") → 97 |
| string.len | 获取字节长度 | string.len("abc") → 3 |
| string.rep | 重复字符串n次 | string.rep("ab",3) → "ababab" |
| gbkToUtf8 | GBK 编码字符串转 UTF-8 | local s = gbkToUtf8(gbkStr) |
| cleanText | 清理文本中的空格/换行/特殊字符(OCR 结果清洗常用) | local t = cleanText(ocrText) |
UTF-8 扩展方法(支持中文)
| 函数名 | 说明 | 示例 |
|---|---|---|
| utf8.inStr | UTF-8查找子串 | utf8.inStr(1, "中国人民", "人民") |
| utf8.inStrRev | UTF-8反向查找 | utf8.inStrRev("中国人民人民", "人民") |
| utf8.strReverse | UTF-8字符串反转 | utf8.strReverse("中国人民") |
| utf8.length | UTF-8字符长度 | utf8.length("中国人民万岁") |
| utf8.left | 从左截取n个字符 | utf8.left("中国人民", 2) |
| utf8.right | 从右截取n个字符 | utf8.right("中国人民", 2) |
| utf8.mid | 中间截取 | utf8.mid("中国人民", 2, 2) |
| utf8.strCut | 移除指定范围字符 | utf8.strCut("中国人民", 2, 2) |
📊 数组方法 7个API
| 函数名 | 说明 | 示例 |
|---|---|---|
| # (数组长度) | 获取数组元素个数 | local len = #arr |
| table.concat | 连接数组元素为字符串 | table.concat({"a","b"}, ",") → "a,b" |
| table.insert | 插入元素 | table.insert(arr, "x") / table.insert(arr, 1, "x") |
| table.remove | 移除元素 | table.remove(arr, 2) → 移除第2个元素 |
| table.sort | 数组排序 | table.sort(nums) / table.sort(nums, function(a,b) return a>b end) |
| table.unique | 数组去重 | local uniq = table.unique(arr) |
| table.merge | 合并两个数组/表 | local all = table.merge(arr1, arr2) |
🔢 数学方法与类型转换 32个API
Lua 标准 math 库与类型转换函数。脚本在设备端执行(LuaJ 3.0.1),同一份脚本在「📦 代码箱调试」与已安装 APK 上结果一致。
math.type / math.tointeger(math.maxinteger) / math.maxinteger 的结果和桌面 Lua 5.5 不同,脚本里不要写死类型或做精确相等判断;② 随机数算法与其他 Lua 实现不同,
math.random 的结果不能按固定数字断言,请用范围判断;③ 浮点数只有约 7 位有效数字,比较小数请用区间(如
v > 0.13 and v < 0.15);需要固定小数位时先 math.floor(v * 100 + 0.5) / 100 再转字符串。
数学方法(math 库)(30个)
| 函数名 | 说明 | 示例 |
|---|---|---|
| math.abs | 返回 x 的绝对值(去掉正负号) | print(math.abs(-3)) → 3 |
| math.ceil | 向上取整,返回大于等于 x 的最小整数 | print(math.ceil(3.1)) → 4 |
| math.floor | 向下取整,返回小于等于 x 的最大整数 | print(math.floor(3.9)) → 3 |
| math.fmod | 取余运算,返回 x 除以 y 的余数(结果符号跟随 x)。注意:Lua 的 % 运算符与 math.fmod 对负数的结果不同,% 的结果符号跟随 y。 | print(math.fmod(10, 3)) → 1 |
| math.modf | 把数字拆成整数部分和小数部分,返回两个值(整数部分, 小数部分)。注意:小数部分是浮点数,直接打印两端精度不同,建议用范围比较。 | local intPart, fracPart = math.modf(3.14) |
| math.max | 返回参数中的最大值,可以传多个参数 | print(math.max(3, 7, 5)) → 7 |
| math.min | 返回参数中的最小值,可以传多个参数 | print(math.min(3, 7, 5)) → 3 |
| math.sqrt | 计算平方根(开平方),x 为负数时结果为 nan | print(math.sqrt(9) == 3) → true |
| math.pow | 计算 x 的 y 次方。写法与 x ^ y 等价,推荐直接使用 ^ 运算符(更简洁) | print(math.pow(2, 10) == 1024) → true |
| math.exp | 返回 e 的 x 次方(e 约等于 2.718281828)。math.exp(0) 等于 1,是 math.log 的反函数 | print(math.exp(0) == 1) → true |
| math.log | 计算对数。只传 x 时是自然对数(以 e 为底);传第 2 个参数表示以该数为底,例如 math.log(x, 10) 是以 10 为底 | print(math.log(1) == 0) → true |
| math.log10 | 以 10 为底的对数,等价于 math.log(x, 10)。常用于计算数字的位数:位数 = math.floor(math.log10(n)) + 1 | print(math.log10(1000) == 3) → true |
| math.deg | 把弧度转换为角度。游戏里坐标计算常用弧度,显示给用户时要转成角度 | print(math.deg(math.pi) == 180) → true |
| math.rad | 把角度转换为弧度。三角函数(sin/cos/tan)的参数都是弧度,所以要先转换 | print(math.rad(180) == math.pi) → true |
| math.sin | 计算正弦值,参数是弧度(角度要先 math.rad 转换)。返回 -1 到 1 之间的浮点数 | print(math.sin(0) == 0) → true |
| math.cos | 计算余弦值,参数是弧度(角度要先 math.rad 转换)。返回 -1 到 1 之间的浮点数 | print(math.cos(0) == 1) → true |
| math.tan | 计算正切值,参数是弧度(角度要先 math.rad 转换)。90 度(pi/2)附近数值会非常大 | print(math.tan(0) == 0) → true |
| math.asin | 反正弦函数,参数必须在 -1 到 1 之间,返回弧度值(可用 math.deg 转成角度) | print(math.asin(0) == 0) → true |
| math.acos | 反余弦函数,参数必须在 -1 到 1 之间,返回弧度值(可用 math.deg 转成角度) | print(math.acos(1) == 0) → true |
| math.atan | 反正切函数。只传 y 时返回弧度值(-pi/2 ~ pi/2);传两个参数 math.atan(y, x) 时可以判断象限,等价于 math.atan2(y, x)(坐标计算推荐用两参数形式) | print(round2(math.atan(1)) == 0.79) → true(约等于 pi/4) |
| math.atan2 | 根据 y、x 坐标求弧度值,返回值范围 -pi ~ pi,能正确判断象限(angle 类的坐标计算推荐用它)。等价于 math.atan(y, x) | print(round2(math.atan2(1, 1)) == 0.79) → true(约等于 pi/4) |
| math.random | 生成随机数。不传参数返回 0 ~ 1 之间的随机小数;math.random(n) 返回 1 ~ n 的随机整数;math.random(m, n) 返回 m ~ n 的随机整数。⚠️ 随机数序列与桌面 Lua 不同,即使种子相同,得到的数字也不一样,脚本里请用范围判断,不要写死具体数字 | local r = math.random() |
| math.randomseed | 设置随机数种子。同一台设备上固定种子可以复现同一串随机数(脚本里常用固定种子让测试结果可重复),不设置时每次运行的结果都不同。⚠️ 只保证「同一台设备 + 同一种子可复现」,换成别的 Lua 实现产生的序列并不相同 | math.randomseed(12345) |
| math.pi | 圆周率 π(约 3.141592653589793)的常量,直接用即可。注意:安卓端 LuaJ 只有 7 位有效数字(约 3.1415927),不要用它做高精度比较 | print(math.pi > 3.14159 and math.pi < 3.1416) → true |
| math.huge | 无穷大常量(大于任何数字)。可用于初始化「最小值」或表示无限循环条件;-math.huge 表示负无穷 | print(math.huge > 1e308) → true |
| math.maxinteger | 整数最大值(9223372036854775807,即 2^63-1)。⚠️ 安卓端 LuaJ 的数字是双精度浮点,此值约为 9.223372036854776e18,做精确相等判断时请用本页示例的写法 | print(math.maxinteger > 0) → true |
| math.mininteger | 整数最小值(-9223372036854775808,即 -2^63) | print(math.mininteger < 0) → true |
| math.type | 判断数字是整数(integer)还是浮点数(float),非数字返回 nil。⚠️ 设备端 LuaJ 只有双精度浮点,无法区分 3 与 3.0:math.type(3.0)、math.type(10 / 2)、math.type(math.maxinteger) 在设备端都按 integer 处理,脚本里不要依赖这个行为 | print(math.type(3)) → integer |
| math.tointeger | 把 x 转换成整数:数值上等于整数时返回整数,否则返回 nil(不会四舍五入)。数字字符串也能转换,常用于把界面输入的文字安全地变成数字。⚠️ 安卓端受浮点表示限制,math.tointeger(math.maxinteger) 会返回 nil | print(math.tointeger(3.0)) → 3 |
| math.ult | 按「无符号整数」比较 m 是否小于 n。普通比较里 -1 小于 0,但无符号比较会把 -1 当成极大值,所以 math.ult(-1, 0) 结果是 false。只在处理内存地址、位运算等场景才需要 | print(math.ult(1, 2)) → true(普通比较也成立) |
类型转换(tostring / tonumber)(2个)
| 函数名 | 说明 | 示例 |
|---|---|---|
| tostring | 把任意值转换成字符串,常用于拼接日志或与文字比较。数字、布尔、nil 都能直接转;table/function 会转成地址字符串,没有实际意义。⚠️ 设备端 LuaJ 的浮点数只保留约 7 位有效数字(1/3 显示为 0.3333333),需要固定小数位时请先 math.floor(v * 100 + 0.5) / 100 再转字符串 | print(tostring(123)) → 123 |
| tonumber | 把字符串转换成数字,转换失败返回 nil(不会抛错)。支持十进制、十六进制(0x1A)、科学计数法(1e3)和前后空格;第 2 个参数可指定进制(2 ~ 36),指定进制时按该进制解析(如 16 进制可写 ff)。⚠️ 第 2 个参数只在第一个参数是字符串时使用:tonumber(7, 10) 会返回 7(不会报错) | print(tonumber("123")) → 123 |
📋 JSON方法 2个API
local data = json_parse('{"name":"test","value":123}') log("名称: " .. data.name) -- 输出: test
local data = {name = "test", value = 123} local json_str = json_stringify(data) log(json_str) -- 输出: {"name":"test","value":123}
🧵 多线程 14个API
在脚本里开多个线程并行干活:一边等待某个界面出现,一边做别的事。每个线程跑在独立 Java 线程上(真并行),互不阻塞。
_G)是所有线程共享的,多个线程同时改写同一个全局变量可能丢数据,请用 thread_lock() / thread_unlock() 保护临界区;② 停止是协作式的,子线程在调用 sleep / 取色 / 点击等 API 时才响应停止;③ 主脚本结束时所有子线程会自动退出;④ 最多同时 32 个子线程。
... (可选): 传给该函数的参数
-- 用法1:匿名函数(判断_等待服务器出现 放到子线程里跑)
local tid = thread_start(function()
while true do
if 综合调用.判断_服务器() then break end
sleep(100)
end
end)
-- 用法2:函数名(字符串),需与函数定义同名
-- 注意:脚本里的中文函数名会被自动转成英文标识符,字符串写法会找不到,
-- 中文函数请改用函数引用:thread_start(判断_等待服务器出现)
local tid2 = thread_start("judgeServer")
-- 用法3:带参数 + 取返回值
local tid3 = thread_start(function(name, times)
for i = 1, times do
log(name .. " 第" .. i .. "次")
sleep(100)
end
return name .. "完成"
end, "打怪", 3)
----主脚本代码写在这里
-- 主脚本继续做别的事,最后统一等待
thread_wait_all()
log(thread_result(tid3))timeout (number, 可选): 超时毫秒,省略表示一直等待
local tid = thread_start(function() sleep(1000) end)
if thread_wait(tid, 5000) then
log("线程已结束")
else
log("等待超时,线程仍在运行")
end
thread_wait() -- 等待全部子线程thread_start(function() sleep(500) end)
thread_start(function() sleep(800) end)
if thread_wait_all(10000) then
log("全部线程结束")
endlocal tid = thread_start(function()
while true do sleep(100) end
end)
sleep(500)
thread_stop(tid) -- 请求停止
thread_wait(tid, 3000) -- 等它退出thread_start(function() while true do sleep(100) end end)
thread_start(function() while true do sleep(100) end end)
local n = thread_stop_all()
log("已停止 " .. n .. " 个线程")local tid = thread_start(function() sleep(2000) end) log(tostring(thread_is_running(tid))) -- true thread_wait(tid) log(tostring(thread_is_running(tid))) -- false
thread_start(function() sleep(1000) end)
thread_start(function() sleep(1000) end)
log("当前线程数: " .. thread_count()) -- 2
thread_wait_all()
log("当前线程数: " .. thread_count()) -- 0local tid = thread_start(function()
log("我在子线程 " .. thread_id()) -- 例如 1
end)
log("我在主线程 " .. thread_id()) -- 0
thread_wait(tid)thread_start(function() sleep(500) end)
thread_start(function() sleep(500) end)
sleep(100)
for i, tid in ipairs(thread_list()) do
log("存活线程: " .. tid)
endlocal tid = thread_start(function()
sleep(500)
return "结果A", 123
end)
thread_wait(tid)
local a, b = thread_result(tid)
log(tostring(a) .. ", " .. tostring(b)) -- 结果A, 123local tid = thread_start(function()
error("子线程出错了")
end)
thread_wait(tid)
local err = thread_error(tid)
if err then log("线程报错: " .. err) end计数 = 0
local function 累加()
for i = 1, 1000 do
thread_lock() -- 进入临界区
计数 = 计数 + 1
thread_unlock() -- 离开临界区
end
end
thread_start(累加)
thread_start(累加)
thread_wait_all()
log("计数 = " .. 计数) -- 2000(不加锁可能少几次)thread_lock() 共享变量 = 共享变量 + 1 thread_unlock()
local tid = thread_start(function()
for i = 1, 3 do
log("子线程 tick " .. i)
thread_sleep(300)
end
end)
log("主线程不受影响,继续执行")
thread_wait(tid)🪲 断点调试(PC 开发工具)
断点是 PC 开发工具内置的脚本调试功能:在编辑器左侧行号栏点一下,脚本运行到这一行就会暂停,方便你确认脚本到底走到哪里、循环了几次、某段逻辑有没有被执行。断点只保存在本机开发工具里,不会写进 .lua 脚本,也不会影响打包后的 APK。
function xxx() 声明行、else 行也不会暂停;单独的 end 行同样不要设(实测 while / for 的 end 行不产生行事件,个别位置的 end 偶尔命中,位置完全不可靠)。原因:断点依赖 Lua 的 行事件,只有真正被执行到的语句行才会产生行事件。空行和注释行在编译后根本不存在,也不会被执行,所以红点虽然画上去了,脚本却永远走不到“这一行”。典型表现就是:断点明明在
while 循环里,却只停一次或者完全不停(其实是红点画在了循环体里的空行 / 注释行上)。✅ 正确做法:把断点加在可执行语句上 —— 赋值、函数调用、
log(...) / sleep(...)、if / while / for 判断行、return 等。
while true do
-- ===== 任务循环体 ===== -- ❌ 注释行:断点不会命中
-- ❌ 空行:断点不会命中
计次循环 = 计次循环 + 1 -- ✅ 可执行语句:每次循环都命中
log("捉妖-进行中" .. 计次循环) -- ✅ 可执行语句:每次循环都命中
sleep(100 * 10) -- ✅ 可执行语句:每次循环都命中
end -- ❌ 循环的 end 行:断点不会命中(请设在循环体内的语句上)
适用环境(很重要)
| 运行方式 | 断点是否生效 | 说明 |
|---|---|---|
| ✅ 代码箱调试(「📦 代码箱调试」,或连接后按 F5;需要新版代码箱 APK) | 生效 | 断点随 code 消息下发到设备并写入项目目录;设备端 LuaJ 用行钩子暂停,把「项目内相对路径 + 行号」回传,开发工具自动定位并高亮。旧版 APK 会忽略断点,请更新代码箱 |
| ❌ 打包 APK 后在手机或模拟器上独立运行 | 不生效 | 没有开发工具连接,断点只是开发工具里的本地标记,不会写进脚本 |
💡 断点只在「代码箱调试」会话里生效:改完断点后重新点「📦 代码箱调试」或按 F5,断点会随代码一起下发到设备。
添加 / 移除断点
| 操作 | 效果 |
|---|---|
| 鼠标移到行号上 | 显示提示「第 N 行:单击后添加断点 (F9)」,并在行号左侧出现半透明红点 |
| 在空行 / 注释行上单击 | 红点能加上,但永远不会命中(这两类行不产生 Lua 行事件)。断点请改设在可执行语句上,否则脚本会表现为“断点不生效 / 循环里一直不停” |
| 行号上单击鼠标左键 | 添加断点:变成实心红点,提示变为「第 N 行:单击后移除断点 (F9)」 |
| 再次左键单击(或按 F9、编辑器正文右键菜单「切换断点」) | 移除该行断点 |
| F9 | 在当前光标所在行添加 / 移除断点(只作用于当前编辑器标签页) |
| 右键 / 中键点击行号 | 右键只是弹出断点菜单(切换本行 / 清除本文件 / 清除所有断点),不会直接切换断点;中键点击什么都不会发生(避免误操作) |
💡 断点按文件路径自动保存(保存在开发工具的 config/breakpoints.json,与 editor_config.json 同目录)。下次启动开发工具重新打开同一个文件时,红点会自动恢复。
运行到断点后会发生什么
- 脚本暂停:状态栏显示「已暂停」,控制台输出
[断点] 已暂停: 文件名:行号。 - 自动定位:开发工具自动切回代码编辑器、滚动到命中的文件与行;行号左侧显示黄色三角箭头 ▶,整行淡黄高亮。
- 等待操作:此时可以点「继续」运行到下一个断点、点「单步」执行下一行、或点「停止」结束脚本。
- 执行时机:断点停在该行执行之前。也就是说,高亮行里的语句还没执行,日志里还看不到这一行的输出;按「单步 / 继续」之后它才会执行。
function main() local count = 0 for i = 1, 5 do count = count + i -- ← 在这一行(例如第 4 行)的行号上单击添加断点 log("count = " .. count) end log("循环结束") end main()
运行后脚本每次循环走到这一行都会暂停;点「继续」进入下一次循环,重复 5 次后继续执行到「循环结束」。想观察变量值,用 log() 输出即可(暂停时该行还没执行,所以看到的是上一次循环的结果)。
单步执行
暂停后点「单步」,脚本只执行下一行就再次暂停,方便一行一行确认逻辑。单步依赖行级调试,因此运行前至少要有一个断点;没有断点时点单步会提示「未启用行级调试」,并按「继续」处理(不会卡在暂停状态)。
| 暂停后的按钮 | 效果 |
|---|---|
| ▶ 继续 | 继续运行,直到下一个断点命中或脚本结束 |
| ⏭ 单步 | 只执行下一行,然后再次暂停(下一行也会高亮) |
| ⏹ 停止 | 立即终止脚本(暂停状态下也能停) |
编辑代码时断点会自动跟随
- 在断点上方插入整行(回车换行、粘贴多行)→ 断点行号自动 +N,仍然对着原来那条语句。
- 删除整行 → 断点行号自动 -N;如果删掉的就是带断点的行,该断点自动移除。
- 同一行内改字(不增删换行)→ 断点不动。
- 重新加载文件(文件被外部修改后重新打开 / 重载)→ 该文件的断点会被清空,因为行号可能整体错位,避免停在错误的位置。
多文件 / require 子模块的断点
运行前,开发工具会把所有已打开标签页里的断点一起同步给 Lua 引擎。因此:
- 主脚本里的断点可以直接用。
- 被
require/dofile加载的子文件(例如子程序_打怪.lua),只要它在编辑器里打开过并设置了断点,也能暂停;命中时会自动切到那个子文件的标签页并高亮对应行。 - 没有在编辑器里打开的子文件不会被同步(工具不知道它的断点),命中不了是正常现象——把该文件打开、加上断点再运行即可。
- 断点按绝对路径匹配,项目目录里的相对路径
require("lib")也能正确对应到scripts/lib.lua。
性能说明(调试完记得取消断点)
启用断点后,设备端 LuaJ 会安装 Lua 行事件钩子(每执行一行回调一次),以保证每一行都能准确停下来。这会让脚本运行速度明显慢于正常模式(视脚本而定,通常慢数倍)。
断点不生效?按顺序排查
| 现象 | 原因与处理 |
|---|---|
| 脚本直接跑完,完全没有暂停 | ① 断点是在运行之后才加的 → 先停止,设置好断点再运行; ② 代码箱 APK 是旧版(不识别断点) → 更新代码箱后重新点「代码箱调试」/ F5; ③ 打包 APK 后在手机上独立运行 → 没有开发工具连接,不支持断点; ④ 代码箱改完断点后没有重新发送代码 → 断点只在发送 code 时下发。 |
| 断点在某一行,但脚本没停(循环里设了断点却一直不停) | ① 断点设在了空行 / 注释行上(最常见原因) —— 这两类行不产生 Lua 行事件,红点永远不会命中; ② 该行不是可执行语句: function xxx() 声明行、else 行、while / for 的 end 行同样不会停(repeat ... until 的 until 条件行则会停);③ 请把断点放到函数体 / 循环体内部的实际语句上(赋值、函数调用、 log(...)、sleep(...)、if / while / for 判断行)。 |
| 断点行号偏了 / 停到了别的语句 | 文件被外部修改过或刚重载过。重新运行前确认红点位置;必要时重新加载文件(会清空旧断点)后重新添加。 |
| 暂停后不知道变量值 | 在断点前后用 log("变量=" .. tostring(变量)) 输出;当前版本暂停面板只高亮位置,不提供变量浏览器。 |
| 点「单步」没反应 | 本次运行没有启用行级调试(运行前没有任何断点),或当前不是暂停状态。先设置断点并让脚本暂停,再点单步。 |
| 新写的未保存脚本断点不生效 | 未保存的标签页没有 .lua 文件路径,运行时不会走 Lua 引擎。请先「Ctrl+S」保存为 .lua 文件,再设置断点并运行。 |
快捷键与操作一览
| 操作 | 方式 |
|---|---|
| 添加 / 移除断点 | 行号栏左键单击 · F9 · 行号栏右键「第 N 行:添加/移除断点」 |
| 清除断点 | 行号栏右键「清除本文件所有断点」/「🗑 清除所有断点」· 菜单「运行 → 🗑 清除所有断点」(一次清空所有文件,含未打开文件的持久化记录) |
| 运行脚本 | 工具栏「▶ 运行」/ 菜单「运行」· F5 |
| 继续执行(到下一个断点) | 「▶ 继续」按钮 / 菜单「继续」· F8 |
| 暂停运行中的脚本 | 「⏸ 暂停」按钮 · F6(启用行级调试时在下一行立即生效) |
| 单步执行 | 「↪ 单步」按钮 · F10 |
| 停止脚本 | 「⏹ 停止」按钮 · F7(暂停状态下也可用) |
🧩 扩展方法与插件开发 9个API
在脚本里调用 Java 类、加载 APK / .so 插件的扩展能力。这是设备端独有的能力(依赖设备上的 Java 虚拟机),「📦 代码箱调试」和装在设备上的 App 都可用。
java.lang / java.util / java.io 及 android.content/os/app/util/graphics/view/widget、org.json 已自动导入,其它包要先 import('包名.*');② Lua 的 table 传给 Java 会转成 List(纯数组)或 Map(含字符串键),Java 的 List/Map 数组会转回 Lua 表;Lua 函数可以当 Java 接口(Runnable / OnClickListener 等)直接传参;
③ Java 回调可能在别的线程执行,回调里不要直接操作界面,建议用 Handler/runOnUiThread 或写全局变量;
④ 反射调用比纯 Lua 慢,避免在每帧循环里频繁调用;加载插件等于执行外来代码,只加载可信来源(完整说明见下方「扩展方法注意事项」)。
-- 整包导入(之后直接用包里的类名)
import('java.lang.*')
import('java.util.*')
import('android.content.Context')
print(Math.sin(1.2)) -- 静态方法:0.932...
print(System.currentTimeMillis()) -- 静态方法(无参)
print(Integer.MAX_VALUE) -- 静态字段
-- 导入具体类并用短名调用
local StringBuilder = import('java.lang.StringBuilder')
local sb = StringBuilder() -- 构造对象(等价于 StringBuilder.new())
sb.append('hello').append(' world') -- 支持链式调用
log(sb.toString())
-- 导入 Android 类
local Intent = import('android.content.Intent')
local ctx = LuaEngine.getContext()
local it = Intent(ctx, Context.ACTION_VIEW) -- 也可以 Intent(Context.ACTION_VIEW)
log(tostring(it))
-- Lua table 会自动转成 Java 的 List / Map
local list = import('java.util.ArrayList')()
list.addAll({'a', 'b'}) -- table -> List
log(list.size()) -- 2
local map = import('java.util.HashMap')()
map.putAll({name = 'gulong', ver = 1})-- table -> Map
log(map.get('name'))import('android.content.Context')
local ctx = LuaEngine.getContext()
print(ctx.getPackageName()) -- 包名
-- JavaBean 属性写法也支持(等价于 getPackageName())
print(ctx.packageName)
-- 取系统服务:TelephonyManager / Vibrator / WifiManager ...
local tm = ctx.getSystemService(Context.TELEPHONY_SERVICE)
if tm ~= nil then
local imei = tm.getImei(0)
print(imei)
end
-- Application 级上下文(不持有 Service/Activity 引用)
local appCtx = LuaEngine.getApplicationContext()import('java.lang.*')
-- 1) 插件放在脚本目录,只写名字
local loader = LuaEngine.loadApk('app-release.apk')
if loader == nil then
log('插件加载失败')
return
end
-- 2) 加载插件里的类(类里的静态方法/构造/实例方法都能用)
local Plugin = loader.loadClass('com.lrapp.plugin.Plugin')
if Plugin ~= nil then
print(Plugin.add(1, 2)) -- 静态方法
local p = Plugin() -- 构造对象
print(p.getName())
else
log('类不存在: ' .. tostring(loader.lastError))
end
-- 3) 插件带 C/C++ 实现时(APK 内 lib/arm64-v8a/libxxx.so)
loader.loadSo('xxx') -- 等价于 System.loadLibrary("xxx")
local NativeLib = loader.loadClass('com.xxx.NativeLib')
print(NativeLib.version())
-- 4) 没有 .so 时也可以看一下插件里有哪些原生库
for i, name in ipairs(loader.listSo()) do
log(i, name)
end
loader.close()-- 场景1: 插件放脚本目录,直接按函数名调用
local lib = LuaEngine.loadLib('libmyplugin.so')
log(lib.name, lib.count) -- 插件名 / 函数个数
print(lib.add(1, 2)) -- 调用 C/C++ 函数
print(lib.hello('古龙')) -- 字符串参数/返回值
print(lib:call('add', 3, 4)) -- 等价写法(函数名是保留字时用这个)
-- 场景2: 查看插件注册了哪些函数
for i, name in ipairs(lib.functions) do
log(i, name)
end
-- 场景3: 复杂的表数据用 JSON 字符串传
local info = lib.parse('{"x":1,"y":2}')
log(info)
lib.close() -- 卸载插件(脚本结束时自动可省略)-- JNI 标准写法(.so + Java 包装类):
-- 1) 载入原生库
LuaEngine.loadSo('libmyplugin.so')
-- 2) 载入声明了 native 方法的 Java 类
local NativeLib = import('com.myplugin.NativeLib')
print(NativeLib.add(1, 2)) -- 调用 native 静态方法
local obj = NativeLib()
print(obj.version())headers (table, 可选): 请求头键值对,例如 {['User-Agent'] = 'Mozilla/5.0'}
local headers = {}
headers['User-Agent'] = 'Mozilla/5.0 (Linux; Android 13)'
local ret = LuaEngine.httpGet('https://www.baidu.com', headers)
log('长度: ' .. #ret)
-- 也可以 POST JSON(服务器返回 JSON 时用 json 解析)
local body = {device = 'gulong', ver = 1}
local resp = LuaEngine.httpPost('https://example.com/api', body)... (any, 可选): 构造参数
-- 用字符串类名创建(适合类名来自配置的场景)
local clsName = 'java.util.ArrayList'
local list = LuaEngine.newInstance(clsName)
list.add('a')
log(list.size())
-- 用类对象创建
local File = import('java.io.File')
local f = LuaEngine.newInstance(File, '/sdcard/a.txt')
log(f.exists())
-- 取类对象(类不存在返回 nil,不会报错)
local C = LuaEngine.classOf('android.os.Build')
log(C.MODEL, C.VERSION.SDK_INT) -- 静态字段// ===== myplugin.cpp =====
#include "gulong_plugin.h"
#include <string.h>
#include <stdlib.h>
static int l_add(void* ctx, int argc, const GulongValue* argv, GulongValue* result) {
long long a = gulong_arg_int(argv, argc, 0, 0);
long long b = gulong_arg_int(argv, argc, 1, 0);
gulong_result_int(result, a + b);
return 1; // 1=有返回值, 0=返回 nil
}
static int l_hello(void* ctx, int argc, const GulongValue* argv, GulongValue* result) {
const char* who = gulong_arg_string(argv, argc, 0, "world");
// 返回值字符串要长期有效:这里用 static 缓冲
static char buf[256];
snprintf(buf, sizeof(buf), "hello, %s!", who);
gulong_result_string(result, buf);
return 1;
}
static int l_calc(void* ctx, int argc, const GulongValue* argv, GulongValue* result) {
double x = gulong_arg_double(argv, argc, 0, 0);
gulong_result_double(result, x * 3.14159);
return 1;
}
GULONG_EXPORT int gulong_plugin_init(GulongPluginHost* host) {
if (host->api_version != GULONG_PLUGIN_API_VERSION) return 1;
host->add_function(host->ctx, "add", l_add, "两数相加", 2, 2);
host->add_function(host->ctx, "hello", l_hello, "打招呼", 0, 1);
host->add_function(host->ctx, "calc", l_calc, "乘圆周率", 1, 1);
host->log(host->ctx, "myplugin loaded");
return 0; // 0=成功
}
GULONG_EXPORT const char* gulong_plugin_name(void) { return "myplugin"; }
GULONG_EXPORT const char* gulong_plugin_version(void) { return "1.0.0"; }
// ===== Android.mk(放到插件工程 jni/ 目录) =====
// LOCAL_PATH := $(call my-dir)
// include $(CLEAR_VARS)
// LOCAL_MODULE := myplugin
// LOCAL_SRC_FILES := myplugin.cpp
// LOCAL_CPP_FEATURES := exceptions
// LOCAL_LDLIBS := -llog
// include $(BUILD_SHARED_LIBRARY)
// ===== 或 CMakeLists.txt =====
// add_library(myplugin SHARED myplugin.cpp)
// target_link_libraries(myplugin log)
// ===== 编译(NDK r25 示例) =====
// cd 插件工程/jni && ndk-build
// 产物: ../libs/arm64-v8a/libmyplugin.so -> 复制到脚本目录
-- ===== Lua 调用 =====
local lib = LuaEngine.loadLib('libmyplugin.so')
print(lib.add(1, 2)) -- 3
print(lib.hello('古龙精灵')) -- hello, 古龙精灵!
print(lib.calc(2)) -- 6.28318
print(lib.name, lib.version) -- myplugin / 1.0.0
-- 用不到的写法:内置的 lib.count 是函数个数
for i, fn in ipairs(lib.functions) do log(fn) end-- ===== 推荐用法(简写) =====
-- 1) 默认包直接可用,不必 import
log(Math.max(1, 5)) -- 5
log(tostring(java.util.Date())) -- 当前时间
-- 2) 需要其它包先整包导入
import('android.hardware.*')
local SensorManager = import('android.hardware.SensorManager')
local ctx = LuaEngine.getContext()
local sm = ctx.getSystemService(Context.SENSOR_SERVICE)
log(tostring(sm))
-- 3) 回调:Lua 函数当接口用
local Thread = import('java.lang.Thread')
local t = Thread(Runnable(function()
log('子线程执行中')
-- 注意:不要在这里直接操作界面
end))
t.start()
t.join()
-- 4) 错误处理:用 pcall 包住扩展调用
local ok, err = pcall(function()
return import('no.such.Class')
end)
if not ok then log('导入失败: ' .. tostring(err)) end📄 完整脚本示例
以下是一个完整的游戏挂机脚本示例,演示了触控、找图、取色、OCR、用户设置等核心功能的使用。
-- ============================================================ -- 梦幻逍遥脚本 - 核心逻辑示例 -- 演示:触控、取色、OCR、用户设置获取 -- ============================================================ -- 加载子模块 dofile("settings_fields.lua") dofile("server_api.lua") local 打怪调用 = require("子程序_打怪") local 综合调用 = require("子程序_综合调用") local 组队调用 = require("子程序_组队") -- 判断是否正在战斗中(取色方式) function 判断_人物停止了() -- 取色判断:如果画面中某特征颜色不变,说明人物停在原处 local colorStr = get_color(451, 585) local r, g, b = tonumber("0x"..colorStr:sub(1,2)), tonumber("0x"..colorStr:sub(3,4)), tonumber("0x"..colorStr:sub(5,6)) sleep(900) local colorStr2 = get_color(451, 585) local r1, g1, b1 = tonumber("0x"..colorStr2:sub(1,2)), tonumber("0x"..colorStr2:sub(3,4)), tonumber("0x"..colorStr2:sub(5,6)) -- 颜色不变 = 人物已停止 if r == r1 and g == g1 and b == b1 then return true end return false end -- 主函数 function main() log("==================================================") log(" 古龙精灵 - 梦幻逍遥") -- 获取用户设置 local settings = get_user_settings() -- ⚠️ 中文key必须用方括号语法 local auto_fight_自动饱食度 = settings["auto_fight_自动饱食度"] local auto_自动巡山 = settings["auto_自动巡山"] local auto_自动除妖 = settings["auto_自动除妖"] local auto_fight_自动打怪 = settings["auto_fight_自动打怪"] -- 主循环 while true do 综合调用.操作_关闭异常提示弹出() if auto_自动除妖 then log("⚔️ 自动除妖开始!") 主程序调用.主程序_除妖() end if auto_自动巡山 then log("⭐ 自动巡山开始!") 主程序调用.主程序_巡山() end if auto_fight_自动打怪 then log("⚔️ 自动打怪开始!") 主程序调用.主程序_除妖() end sleep(1000) end end main()
抓取工具使用技巧
在 PC 开发工具的工具 → 抓取窗口中:
- 右键点击画面 → 取色:自动生成
get_color(x, y)代码 - 左键框选区域:自动生成
findMultiColor代码(多点取色) - Ctrl+左键自定义取色:手动添加取色点,生成多点取色代码
- 右键 → ONNX查找文字:自动生成
ocr_find_text_local代码 - 左键框选 + OCR模式:自动生成
ocr_recognize_local代码
代码插入方式
在开发工具的右侧 API参考面板和UI设计面板中,点击任意代码即可自动插入到编辑器并复制到剪贴板。