Java - FFM API 实现扫雷助手

文章目录

  • 前言
  • 环境
  • 思路
  • 实现
    • 扫雷常量
      • 高度/宽度/雷数
      • 地图基址
    • 屏幕坐标
  • 效果
  • 资源


前言

使用 FFM API 实现扫雷助手.
扫雷英雄榜

环境

Win11
JDK 21

思路

  1. 读取扫雷地图数据
  2. 判断该数据是否为雷
  3. 模拟鼠标点击
  4. 重复上面操作遍历地图直至完成

确定了思路,那么就要确认 windows 系统提供了哪些函数可以实现,经过网络搜索得到以下函数:

  • ReadProcessMemory 函数 (memoryapi.h):读取内存数据
    • FindWindowW 函数 (winuser.h):查找窗口句柄,根据该窗口句柄获取 pid
    • GetWindowThreadProcessId 函数 (winuser.h):获取 pid
    • openProcess 函数 (processthreadsapi.h):进程的打开句柄
  • PostMessageW 函数 (winuser.h):发送消息模拟鼠标操作

实现

import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;import static java.lang.foreign.MemorySegment.NULL;
import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.ValueLayout.JAVA_LONG;SymbolLookup user32 = SymbolLookup.libraryLookup("User32", Arena.global());
SymbolLookup kernel32 = SymbolLookup.libraryLookup("Kernel32", Arena.global());/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-findwindoww">FindWindowW 函数 (winuser.h)</a>* 检索其类名和窗口名称与指定字符串匹配的顶级窗口的句柄。 此函数不搜索子窗口。 此函数不执行区分大小写的搜索。*/
MethodHandle findWindowW_MH = find(user32, "FindWindowW", FunctionDescriptor.of(// 如果函数成功,则返回值是具有指定类名称和窗口名称的窗口的句柄。ADDRESS,// LPCWSTR lpClassName 类名ADDRESS,// LPCWSTR lpWindowName 程序名ADDRESS
));/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid">GetWindowThreadProcessId 函数 (winuser.h)</a>* 检索创建指定窗口的线程的标识符,以及创建该窗口的进程(可选)的标识符。*/
MethodHandle getWindowThreadProcessId = find(user32, "GetWindowThreadProcessId", FunctionDescriptor.of(// DWORD 如果函数成功,则返回值是创建窗口的线程的标识符。 如果窗口句柄无效,则返回值为零。 要获得更多的错误信息,请调用 GetLastError。ADDRESS,// HWND hWnd 窗口的句柄。ADDRESS,// LPDWORD lpdwProcessId 指向接收进程标识符的变量的指针。 如果此参数不为 NULL, 则 GetWindowThreadProcessId 会将进程的标识符复制到变量;否则,它不会。 如果函数失败,则变量的值保持不变。ADDRESS
));/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess">openProcess 函数 (processthreadsapi.h)</a>* 打开现有的本地进程对象。*/
MethodHandle openProcess = find(kernel32, "OpenProcess", FunctionDescriptor.of(// 如果函数成功,则返回值是指定进程的打开句柄。ADDRESS,// DWORD dwDesiredAccess 对进程对象的访问。 根据进程的安全描述符检查此访问权限。JAVA_INT,// BOOL bInheritHandle 如果此值为 TRUE,则此进程创建的进程将继承句柄。JAVA_INT,// DWORD dwProcessId 要打开的本地进程的标识符。JAVA_INT
));/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory">ReadProcessMemory 函数 (memoryapi.h)</a>*/
MethodHandle readProcessMemory = find(kernel32, "ReadProcessMemory", FunctionDescriptor.of(// 如果该函数成功,则返回值为非零值。JAVA_INT,// HANDLE  hProcess 包含正在读取的内存的进程句柄。ADDRESS,// LPCVOID lpBaseAddress 指向从中读取的指定进程中基址的指针。ADDRESS,// LPVOID  lpBuffer 指向从指定进程的地址空间接收内容的缓冲区的指针。ADDRESS,// SIZE_T  nSize 要从指定进程读取的字节数。JAVA_LONG,// SIZE_T  *lpNumberOfBytesRead 指向变量的指针,该变量接收传输到指定缓冲区的字节数。 如果 lpNumberOfBytesRead 为 NULL,则忽略 参数。ADDRESS
));/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-postmessagew">PostMessageW 函数 (winuser.h)</a>* 将 (帖子) 与创建指定窗口的线程关联的消息队列中,并在不等待线程处理消息的情况下返回消息。*/
MethodHandle postMessageW = find(user32, "PostMessageW", FunctionDescriptor.of(// 如果该函数成功,则返回值为非零值。JAVA_INT,// HWND hWnd 窗口的句柄,其窗口过程是接收消息。ADDRESS,// UINT Msg 要发布的消息。JAVA_INT,// WPARAM wParam 其他的消息特定信息。shift/ctrl 键信息JAVA_INT,// LPARAM lParam 其他的消息特定信息。高位屏幕 y 坐标, 低位屏幕 x 坐标JAVA_INT
));MethodHandle find(SymbolLookup symbolLookup, String functionName, FunctionDescriptor functionDescriptor) {return Linker.nativeLinker().downcallHandle(symbolLookup.find(functionName).orElseThrow(() -> new RuntimeException(STR."\{functionName} Not Found")), functionDescriptor);
}MemorySegment winmineL(Arena arena) {// https://stackoverflow.com/questions/66072117/why-does-windows-use-utf-16levar bs = "扫雷\0".getBytes(StandardCharsets.UTF_16LE);var ms = arena.allocate(bs.length);MemorySegment.copy(bs, 0, ms, JAVA_BYTE, 0, bs.length);return ms;
}/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/procthread/process-security-and-access-rights">进程对象的所有可能的访问权限。</a>*/
static final int PROCESS_ALL_ACCESS = 0x000F_0000 | 0x0010_0000 | 0xFFFF;
static final int BOOL_FALSE = 0;/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-lbuttondown">按下鼠标左键</a>*/
static final int WM_LBUTTONDOWN = 0x0201;
/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-lbuttonup">释放鼠标左键</a>*/
static final int WM_LBUTTONUP = 0x0202;
/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-rbuttondown">按下鼠标右键</a>*/
static final int WM_RBUTTONDOWN = 0x0204;
/*** <a href="https://learn.microsoft.com/zh-cn/windows/win32/inputdev/wm-rbuttonup">释放鼠标右键</a>*/
static final int WM_RBUTTONUP = 0x0205;/*** 雷区高度基址*/
static final MemorySegment MINE_HIGH_BASE = MemorySegment.ofAddress(0x1005338);
/*** 雷区宽度基址*/
static final MemorySegment MINE_WIDTH_BASE = MemorySegment.ofAddress(0x1005334);
/*** 雷区雷数基址*/
static final MemorySegment MINE_NUM_BASE = MemorySegment.ofAddress(0x01005330);
/*** 雷区第一个格子基址*/
static final MemorySegment MINE_FIRST_BASE = MemorySegment.ofAddress(0x01005361);/*** 雷区每行长度*/
static final long LEN_PER_ROW = 32L;/*** 雷值*/
static final byte MINE_VALUE = (byte) 0x8F;/*** 雷区地图 X 基础坐标*/
static final int MINE_X_POS_BASE = 19;
/*** 雷区地图 Y 基础坐标*/
static final int MINE_Y_POS_BASE = 62;
/*** 雷区格子宽度*/
static final int MINE_GRID_WIDTH = 16;static final int SHIFT = 16;
/*** 屏幕缩放比例*/
static final double SCALE = 1;void main() throws Throwable {try (var arena = Arena.ofConfined()) {var windowHandleMS = (MemorySegment) findWindowW_MH.invokeExact(NULL, winmineL(arena));if (NULL.equals(windowHandleMS)) {System.err.println("扫雷程序未启动,退出助手");System.exit(-1);}System.out.println("扫雷程序已启动");var pidMS = arena.allocate(JAVA_INT);var _ = (MemorySegment) getWindowThreadProcessId.invokeExact(windowHandleMS, pidMS);if (NULL.equals(pidMS)) {System.err.println("扫雷获取 pid 失败, 退出助手");System.exit(-2);}var pid = pidMS.getAtIndex(JAVA_INT, 0);final var mineHandleMS = (MemorySegment) openProcess.invokeExact(PROCESS_ALL_ACCESS, BOOL_FALSE, pid);if (NULL.equals(mineHandleMS)) {System.err.println("扫雷 HANDLE 获取失败, 退出助手");System.exit(-3);}var highMS = arena.allocate(JAVA_INT);var _ = (int) readProcessMemory.invokeExact(mineHandleMS, MINE_HIGH_BASE, highMS, JAVA_INT.byteSize(), NULL);var high = highMS.getAtIndex(JAVA_INT, 0);var widthMS = arena.allocate(JAVA_INT);var _ = (int) readProcessMemory.invokeExact(mineHandleMS, MINE_WIDTH_BASE, widthMS, JAVA_INT.byteSize(), NULL);var width = widthMS.getAtIndex(JAVA_INT, 0);var nMineMS = arena.allocate(JAVA_INT);var _ = (int) readProcessMemory.invokeExact(mineHandleMS, MINE_NUM_BASE, nMineMS, JAVA_INT.byteSize(), NULL);var nMine = nMineMS.getAtIndex(JAVA_INT, 0);System.out.println(STR."PID: \{pid}, 屏幕缩放比例:\{SCALE} 行数(高):\{high},列数(宽):\{width},雷数:\{nMine}");final var mapSize = LEN_PER_ROW * high;var mapMS = arena.allocate(mapSize);var _ = (int) readProcessMemory.invokeExact(mineHandleMS, MINE_FIRST_BASE, mapMS, mapSize, NULL);var map = mapMS.toArray(JAVA_BYTE);for (var h = 0; h < high; h++) {for (var w = 0; w < width; w++) {var value = map[(int) (h * LEN_PER_ROW + w)];System.out.print(MINE_VALUE == value ? 'X' : 'O');}System.out.println();}// 扫雷for (var h = 0; h < high; h++) {for (var w = 0; w < width; w++) {var xPos = (int) ((MINE_X_POS_BASE + w * MINE_GRID_WIDTH) * SCALE);var yPos = (int) ((MINE_Y_POS_BASE + h * MINE_GRID_WIDTH) * SCALE);var lParam = (yPos << SHIFT) + xPos;var value = map[(int) (h * LEN_PER_ROW + w)];if (MINE_VALUE == value) {// 雷var _ = (int) postMessageW.invokeExact(windowHandleMS, WM_RBUTTONDOWN, 0, lParam);var _ = (int) postMessageW.invokeExact(windowHandleMS, WM_RBUTTONUP, 0, lParam);} else {var _ = (int) postMessageW.invokeExact(windowHandleMS, WM_LBUTTONDOWN, 0, lParam);var _ = (int) postMessageW.invokeExact(windowHandleMS, WM_LBUTTONUP, 0, lParam);}}}}
}

注意:上面代码使用了 JDK 21 的最新特性,需要开启 --enable-preview

扫雷常量

雷区数据常量需要通过Cheat Engine(CE)去进行获取。

高度/宽度/雷数

通过自定义雷区修改下面值然后在 CE 里去查找明确的值获取其基址。
自定义雷区9-9-10
CE 第一次初始化查找
CE-01
CE 查找 9
CE-02
在自定义雷区修改高度数据
H24
CE 查找 24
CE-24
发现这两个地址都可以使用,于是选择了第一个 0x1005338。
宽度和雷数通过同样的方法最后得到
CE-高度/宽度/雷数

地图基址

接下来,要找到下面几个地址
ABC
首先查找 A 基址:
A1
A2
A3
不断重复上面操作,最终可以得到
A 的基址为:0x01005361
B

B 的基址为:0x01005362
C
C 的基址为:0x01005381
从初级切换到中级、高级,A/B/C的地址均未变化。
因此,得到每行数据长度为 32,每格基址间距为 1

屏幕坐标

接下来,需要获取屏幕坐标计算 PostMessageW 函数参数,这个可以通过 spy++ 获取。

spy++
设置要记录的消息类型
R
L
然后点击 “扫雷” 程序第一个方格中心会得到以下信息:
第一方格中心
这样可以得到 xPosyPos,以此为基准:

/*** 雷区地图 X 基础坐标*/
static final int MINE_X_POS_BASE = 19;
/*** 雷区地图 Y 基础坐标*/
static final int MINE_Y_POS_BASE = 62;
/*** 雷区格子宽度*/
static final int MINE_GRID_WIDTH = 16;

效果

最终,执行代码:
9x9
16x16
16x30

资源

  • winmine-helper
  • Project Panama: Interconnecting JVM and native code
  • ReadProcessMemory 函数 (memoryapi.h)
  • FindWindowW 函数 (winuser.h)
  • GetWindowThreadProcessId 函数 (winuser.h)
  • openProcess 函数 (processthreadsapi.h)
  • PostMessageW 函数 (winuser.h)
  • Cheat Engine

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/619515.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Rust-模式解构

match 首先&#xff0c;我们看看使用match的最简单的示例&#xff1a; exhaustive 有些时候我们不想把每种情况一一列出&#xff0c;可以用一个下划线来表达“除了列出来的那些之外的其他情况”&#xff1a; 下划线 下划线还能用在模式匹配的各种地方&#xff0c;用来表示…

Java+Mysql存储过程生成订单序列号

Mysql存储过程生成唯一订单号 直接上代码&#xff01;&#xff01; 1.创建存储过程 CREATE DEFINERrootlocalhost PROCEDURE getOrderSerialNo(# 前缀in orderPrefix varchar(64), # 返回结果out result int) BEGIN# 当前流水号declare curOrderNo int;# 默认值为0declare e…

uniapp小程序超出一行显示...并展示更多按钮

注意:全部标签需要浮动在父盒子右边哦 循环获取所有需要展示数据标签的高度 this.goods this.goods.map(item > ({...item,showBtn: false}));this.$nextTick(() > {uni.createSelectorQuery().in(this).selectAll(".cart-info").boundingClientRect((data)…

yolov7中断训练后继续训练

1、训练指令 &#xff08;1&#xff09;添加resume参数&#xff0c;参数值改为true &#xff08;2&#xff09;weights参数&#xff0c;参数值改为中断前上次训练权重 中断后继续训练命令&#xff1a; python.exe train.py --weights runs/train/exp9/weights/last.pt --re…

SpringBoot3 WebFlux 可观测最佳实践

前言 链路追踪是可观测性软件系统的一个非常好的工具。它使开发人员能够了解应用程序中和应用程序之间不同交互发生的时间、地点和方式。同时让观测复杂的软件系统变得更加容易。 从Spring Boot 3开始&#xff0c;Spring Boot 中用于链路追踪的旧 Spring Cloud Sleuth 解决方…

Unity2022.3打包Android后从AB包加载场景发现丢失大量脚本问题

问题 这两天遇到一个问题&#xff0c;在VR项目打包Android的时候&#xff0c;加载场景后&#xff0c;Timeline工作不正常&#xff0c;找不到原因。 现象 看到有很多警告&#xff0c;丢失脚本的Log。 因为场景本身也有一些丢失的脚本所以没在意&#xff0c;但是又不是所有脚本…

计算机毕业设计-----SpringBoot招聘网站项目

项目介绍 SpringBoot招聘网站项目。主要功能说明&#xff1a; 管理员登录,简历管理,问答管理,职位管理,用户管理,职位申请进度更新,查看简历等功能。 用户角色包含以下功能&#xff1a;用户首页,登录注册,职位查看,职位详情,投递简历,查看我的申请,管理个人简历,附件简历管理…

紫光展锐M6780丨画质增强——更炫的视觉体验

智能显示被认为是推动数字化转型和创新的重要技术之一。研究机构数据显示&#xff0c;预计到2035年底&#xff0c;全球智能显示市场规模将达到1368.6亿美元&#xff0c;2023-2035年符合年增长率为36.4%。 随着消费者对高品质视觉体验的需求不断增加&#xff0c;智能手机、平板…

如何用ChatGPT写教案设计?以“沁园春雪”为例

1. 引言 随着人工智能技术的飞速发展&#xff0c;ChatGPT已成为教育领域的一大创新工具。ChatGPT不仅能够模拟人类对话&#xff0c;还可以帮助设计互动丰富、内容丰富的教案。本文将探索如何利用ChatGPT进行教案教学设计&#xff0c;特别是通过“沁园春雪”这一案例&#xff0…

基于反卷积方法的重大突破:结构光系统中的测量误差降低3倍

作者&#xff1a;小柠檬 | 来源&#xff1a;3DCV 在公众号「3DCV」后台&#xff0c;回复「原论文」可获取论文pdf 结构光三维测量技术在工业自动化、逆向工程和图形学领域越来越受欢迎。然而&#xff0c;现有的测量系统在成像过程中存在不完美&#xff0c;会导致在不连续边缘周…

【学习心得】Git深入学习

一、深入学习Git必须熟悉两个概念 &#xff08;1&#xff09;【四个区】Git本地有三个区&#xff0c;远程仓库也可以看出成一个区域 工作区、暂存区、本地仓库、远程仓库。 通过四句话来充分理解这三个区 第一句话&#xff1a;你创建的一个文件夹&#xff0c;并且将它初始化…

基于Java SSM框架实现体育竞赛成绩管理系统项目【项目源码+论文说明】

基于java的SSM框架实现体育竞赛成绩管理系统演示 摘要 体育竞赛是各种体育体育项目比赛的总称。是在裁判员的主持下&#xff0c;按统一的规则要求&#xff0c;组织与实施的体育员个体或体育队之间的竞技较量&#xff0c;是竞技体育与社会发生关联&#xff0c;并作用于社会的媒…

基于OpenCV的谷物颗粒识别

基于OpenCV的谷物颗粒识别 一、程序整体功能介绍1.1 导入库与函数定义1.2 颜色分割与灰度处理1.3 二值化与轮廓检测1.4 绘制与计数1.5 主程序与结果展示 二、算法原理与实现流程2.1算法原理&#xff08;1&#xff09;颜色分割&#xff08;2&#xff09;灰度处理与二值化&#x…

前端 TS 语法 接口(2)

介绍 TypeScript的核心原则之一是对值所具有的shape进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里&#xff0c;接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。 只读属性 readonly 一些对象属性只能在对象刚刚创建的…

Java反转单链表

/*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; }* }*/ //核心思想&#xff0c;利用cur和Curnex…

基础篇_数据持久化(实战-我的B站,MySQL数据库)

文章目录 一. 实战-我的B站1. 功能演示2. 设计数据类数据展示路径参数 3. 设计 Service 类静态资源映射读取文件的时机Stream API 改进 二. MySQL 数据库1. 数据库必要性2. MySQL 安装下载压缩包初始化数据库运行服务器运行客户端 3. 初步使用4. datagrip添加数据源导入数据用 …

HBase 基础

HBase 基础 HBase1. HBase简介1.1 HBase定义1.2 HBase数据模型1.2.1 HBase逻辑结构1.2.2 HBase物理存储结构1.2.3 数据模型 1.3 HBase基本架构 2. HBase环境安装2.1 HBase 安装部署2.1.1 HBase 本地按照2.1.2 HBase 伪分布模式安装2.1.3 HBase 集群安装 2.2 HBase Shell操作2.2…

jar包部署到linux虚拟机的docker中之后连不上mysql

前言&#xff1a; 跟着黑马学习docker的时候&#xff0c;将java项目部署到了docker中&#xff0c;运行访问报错&#xff0c;反馈连不上mysql。 错误描述&#xff1a; 方法解决&#xff1a; 概述&#xff1a;在虚拟中中&#xff0c;我进入项目容器的内部&#xff0c;尝试ping…

分布式搜索引擎--认识

elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大功能&#xff0c;可以帮助我们从海量数据中快速找到需要的内容 。 elasticsearch结合kibana、Logstash、Beats&#xff0c;也就是elastic stack&#xff08;ELK&#xff09;。被广泛…

【嵌入式移植】3、编译U-Boot

编译U-Boot 0 U-Boot及本文所选硬件1 获取U-Boot源码2 获取工具链3 BL314 编译4.1 yylloc4.2 u_boot_dtsi 5 烧写6 上电验证 0 U-Boot及本文所选硬件 Das U-Boot&#xff0c;全称 Universal Boot Loader&#xff0c;是遵循GPL条款的开放源码项目。U-Boot的作用是系统引导。U-B…