|
|
package xyz.wbsite.jmacro;
|
|
|
|
|
|
import cn.hutool.core.util.RandomUtil;
|
|
|
import org.sikuli.basics.Settings;
|
|
|
import org.sikuli.script.FindFailed;
|
|
|
import org.sikuli.script.Image;
|
|
|
import org.sikuli.script.Key;
|
|
|
import org.sikuli.script.KeyModifier;
|
|
|
import org.sikuli.script.Location;
|
|
|
import org.sikuli.script.Match;
|
|
|
import org.sikuli.script.Mouse;
|
|
|
import org.sikuli.script.Pattern;
|
|
|
import org.sikuli.script.Region;
|
|
|
import org.sikuli.script.Screen;
|
|
|
import xyz.wbsite.jmacro.base.ColorLocation;
|
|
|
import xyz.wbsite.jmacro.base.Legend;
|
|
|
import xyz.wbsite.jmacro.util.Logger;
|
|
|
import xyz.wbsite.jmacro.util.MousePathUtil;
|
|
|
import xyz.wbsite.jmacro.util.TaskUtil;
|
|
|
|
|
|
import java.awt.*;
|
|
|
import java.awt.datatransfer.Clipboard;
|
|
|
import java.awt.datatransfer.StringSelection;
|
|
|
import java.util.List;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
/**
|
|
|
* 脚本
|
|
|
*
|
|
|
* @author wangbing
|
|
|
* @version 0.0.1
|
|
|
* @since 1.8
|
|
|
*/
|
|
|
public abstract class JMacro {
|
|
|
|
|
|
/**
|
|
|
* 脚本执行状态
|
|
|
*/
|
|
|
private boolean run;
|
|
|
|
|
|
/**
|
|
|
* 默认超时秒数
|
|
|
*/
|
|
|
private int defaultTimeOut = 10;
|
|
|
|
|
|
/**
|
|
|
* 工作区域
|
|
|
*/
|
|
|
protected Region workRegion;
|
|
|
|
|
|
/**
|
|
|
* 屏幕对象
|
|
|
*/
|
|
|
protected Screen screen = new Screen();
|
|
|
|
|
|
public JMacro() {
|
|
|
this.startFocus();
|
|
|
// 禁止漂移,所有拟人操作都提前计算好路径,此处的随机要关闭
|
|
|
Mouse.setRandom(0);
|
|
|
// 鼠标移动延迟(秒),默认值约为 0.5
|
|
|
Settings.MoveMouseDelay = 0.0f;
|
|
|
// 忽略鼠标移动事件
|
|
|
Mouse.setMouseMovedAction(0);
|
|
|
}
|
|
|
|
|
|
public boolean isRun() {
|
|
|
return run;
|
|
|
}
|
|
|
|
|
|
public void setRun(boolean run) {
|
|
|
this.run = run;
|
|
|
}
|
|
|
|
|
|
public int getDefaultTimeOut() {
|
|
|
return defaultTimeOut;
|
|
|
}
|
|
|
|
|
|
public void setDefaultTimeOut(int defaultTimeOut) {
|
|
|
this.defaultTimeOut = defaultTimeOut;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 开始聚焦
|
|
|
*/
|
|
|
public void startFocus() {
|
|
|
TaskUtil.asyncTask(() -> {
|
|
|
workRegion = TaskUtil.timeTask(this::focus, defaultTimeOut, TimeUnit.SECONDS);
|
|
|
if (workRegion != null) {
|
|
|
Logger.info("聚焦成功");
|
|
|
} else {
|
|
|
Logger.error("聚焦失败");
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取工作区域
|
|
|
*
|
|
|
* @return 工作区域
|
|
|
*/
|
|
|
public abstract Region focus();
|
|
|
|
|
|
/**
|
|
|
* 执行脚本
|
|
|
*/
|
|
|
public void start() {
|
|
|
if (this.run) {
|
|
|
Logger.error("脚本正在运行中");
|
|
|
return;
|
|
|
}
|
|
|
this.run = true;
|
|
|
this.task();
|
|
|
this.stop();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 执行脚本
|
|
|
*/
|
|
|
public abstract void task();
|
|
|
|
|
|
/**
|
|
|
* 停止脚本
|
|
|
*/
|
|
|
public void stop() {
|
|
|
if (!this.run) {
|
|
|
Logger.error("脚本未运行");
|
|
|
return;
|
|
|
}
|
|
|
this.run = false;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 键盘按键组
|
|
|
*/
|
|
|
public void keyInput(String keys) {
|
|
|
this.screen.type(keys);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 键盘按键
|
|
|
* keycode Key to press (e.g. <code>KeyEvent.VK_A)
|
|
|
*/
|
|
|
public void keyPress(String key) {
|
|
|
this.screen.type(key);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标移动
|
|
|
*
|
|
|
* @param location 坐标点
|
|
|
*/
|
|
|
public void mouseMove(Location location) {
|
|
|
List<int[]> path = MousePathUtil.generateBezierPath(Mouse.at(), location);
|
|
|
Settings.MoveMouseDelay = 0.0f;
|
|
|
|
|
|
// 暂停点索引(-1为不暂停)
|
|
|
int pause = -1;
|
|
|
// 通过概率决断本次是否需要产生暂停
|
|
|
if (RandomUtil.randomInt(0, 100) < 50) {
|
|
|
int startIdx = path.size() / 3;
|
|
|
int endIdx = 2 * path.size() / 3;
|
|
|
pause = RandomUtil.randomInt(startIdx, endIdx);
|
|
|
}
|
|
|
|
|
|
for (int[] p : path) {
|
|
|
delay(7 + RandomUtil.randomInt(-3, 3));
|
|
|
if (pause == path.indexOf(p)) {
|
|
|
Logger.info("拟人操作,停顿片刻", pause);
|
|
|
delay(200 + RandomUtil.randomInt(-100, 200));
|
|
|
}
|
|
|
Mouse.move(new Location(p[0], p[1]));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标移动到这个区域
|
|
|
* <p>
|
|
|
* 增加拟人操作,随机移动鼠标到区域中心附近(而非直接移动到中心)
|
|
|
*
|
|
|
* @param region 区域
|
|
|
*/
|
|
|
public void mouseMove(Region region) {
|
|
|
mouseMove(randomCenter(region));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标左键单击
|
|
|
*/
|
|
|
public void mouseLeftClick() {
|
|
|
Mouse.at().click();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标左键单击
|
|
|
*
|
|
|
* @param region 区域
|
|
|
*/
|
|
|
public void mouseLeftClick(Region region) {
|
|
|
mouseMove(randomCenter(region));
|
|
|
mouseLeftClick();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标左键单击
|
|
|
*
|
|
|
* @param location 坐标点
|
|
|
*/
|
|
|
public void mouseLeftClick(Location location) {
|
|
|
mouseMove(location);
|
|
|
mouseLeftClick();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标左键双击
|
|
|
*/
|
|
|
public void mouseLeftDoubleClick() {
|
|
|
Mouse.at().doubleClick();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标左键双击
|
|
|
*/
|
|
|
public void mouseLeftDoubleClick(Region region) {
|
|
|
mouseMove(randomCenter(region));
|
|
|
mouseLeftDoubleClick();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标左键双击
|
|
|
*/
|
|
|
public void mouseLeftDoubleClick(Location location) {
|
|
|
mouseMove(location);
|
|
|
mouseLeftDoubleClick();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标右键单击
|
|
|
*/
|
|
|
public void mouseRightClick() {
|
|
|
mouseRightClick(Mouse.at());
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标右键单击
|
|
|
*
|
|
|
* @param region 区域
|
|
|
*/
|
|
|
public void mouseRightClick(Region region) {
|
|
|
mouseMove(randomCenter(region));
|
|
|
mouseRightClick();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 鼠标右键单击
|
|
|
*
|
|
|
* @param location 坐标点
|
|
|
*/
|
|
|
public void mouseRightClick(Location location) {
|
|
|
mouseMove(location);
|
|
|
mouseRightClick();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 插入剪贴板
|
|
|
*/
|
|
|
public void insertClipboard(String text) {
|
|
|
StringSelection selection = new StringSelection(text);
|
|
|
// 获取系统剪贴板
|
|
|
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
|
|
// 将文本设置到剪贴板
|
|
|
clipboard.setContents(selection, null);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 执行复制命令
|
|
|
*/
|
|
|
public void sendCopyCommand() {
|
|
|
screen.type("c", KeyModifier.CTRL);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 执行粘贴命令
|
|
|
*/
|
|
|
public void sendPasteCommand() {
|
|
|
screen.type("v", KeyModifier.CTRL);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 执行回车命令
|
|
|
*/
|
|
|
public void sendEnterCommand() {
|
|
|
screen.type(Key.ENTER);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 捕获指定区域屏幕
|
|
|
*/
|
|
|
public Image capture(Region region) {
|
|
|
return region.getImage();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取聚焦区域
|
|
|
*/
|
|
|
public Region getWorkRegion() {
|
|
|
return workRegion;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取聚焦区域
|
|
|
*/
|
|
|
public Region getScreenRect() {
|
|
|
if (workRegion != null) {
|
|
|
return workRegion;
|
|
|
}
|
|
|
return screen;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 延迟
|
|
|
*/
|
|
|
public void delay() {
|
|
|
delay(100);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 延迟
|
|
|
*/
|
|
|
public void delay(long millis) {
|
|
|
try {
|
|
|
Thread.sleep(millis);
|
|
|
} catch (InterruptedException e) {
|
|
|
throw new RuntimeException(e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 抖动延迟
|
|
|
*/
|
|
|
public void delayUnstable() {
|
|
|
delayUnstable(800);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 抖动延迟
|
|
|
*/
|
|
|
public void delayUnstable(long millis) {
|
|
|
if (millis < 200) {
|
|
|
delay(millis);
|
|
|
return;
|
|
|
}
|
|
|
delay(RandomUtil.randomLong(millis - 100, millis + 100));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 查找图片
|
|
|
*
|
|
|
* @param region 区域范围(不设时,取全屏)
|
|
|
* @param minSimilar 相似度
|
|
|
* @return 匹配区域
|
|
|
*/
|
|
|
public Match findPic(Region region, String legend, double minSimilar) {
|
|
|
Pattern pattern = new Pattern(legend).similar(minSimilar);
|
|
|
return region.findBest(pattern);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 查找图片
|
|
|
*
|
|
|
* @param region 区域范围(不设时,取全屏)
|
|
|
* @param minSimilar 相似度
|
|
|
* @return 匹配区域
|
|
|
*/
|
|
|
public boolean clickPic(Region region, String legend, double minSimilar) {
|
|
|
try {
|
|
|
Pattern pattern = new Pattern(legend).similar(minSimilar);
|
|
|
region.click(pattern);
|
|
|
return true;
|
|
|
} catch (FindFailed e) {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 查找图片
|
|
|
*
|
|
|
* @param legend 图例
|
|
|
* @param minSimilar 最低相似度
|
|
|
* @return 匹配区域
|
|
|
*/
|
|
|
public Region findLegend(String legend, double minSimilar) {
|
|
|
Pattern pattern = new Pattern(of(legend).getFile().getAbsolutePath())
|
|
|
.similar(minSimilar);
|
|
|
Match match = workRegion.findBest(pattern);
|
|
|
if (match == null) {
|
|
|
return null;
|
|
|
}
|
|
|
return new Region(match.getRect());
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 等待并查找图例
|
|
|
*
|
|
|
* @param legend 图例
|
|
|
* @param minSimilar 最低相似度
|
|
|
* @param timeout 超时时间(秒)
|
|
|
* @return 匹配区域
|
|
|
*/
|
|
|
public Region waitAndFindLegend(String legend, double minSimilar, long timeout) {
|
|
|
Logger.info("等待并查找图例:{}", legend);
|
|
|
return TaskUtil.timeTask(() -> {
|
|
|
while (JMainService.getInstance().run) {
|
|
|
Region result = findLegend(legend, minSimilar);
|
|
|
if (result != null) {
|
|
|
return result;
|
|
|
}
|
|
|
}
|
|
|
return null;
|
|
|
}, timeout, TimeUnit.SECONDS);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 查找并点击图例
|
|
|
*
|
|
|
* @param legend 图例
|
|
|
* @param minSimilar 最低相似度
|
|
|
* @return 匹配区域
|
|
|
*/
|
|
|
public boolean clickLegend(String legend, double minSimilar) {
|
|
|
Pattern pattern = new Pattern(of(legend).getFile().getAbsolutePath())
|
|
|
.similar(minSimilar);
|
|
|
Match match = workRegion.findBest(pattern);
|
|
|
if (match == null) {
|
|
|
return false;
|
|
|
}
|
|
|
mouseLeftClick(match.getCenter());
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取图例
|
|
|
*
|
|
|
* @return 图例
|
|
|
*/
|
|
|
public Legend of(String legend) {
|
|
|
return Legend.inflate(legend);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将相对坐标转为绝对坐标
|
|
|
*
|
|
|
* @return 绝对坐标
|
|
|
*/
|
|
|
public Location of(int x, int y) {
|
|
|
int ox = getWorkRegion().getX();
|
|
|
int oy = getWorkRegion().getY();
|
|
|
return new Location(x + ox, y + oy);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将相对坐标转为绝对坐标
|
|
|
*
|
|
|
* @param relativePoint 相对坐标
|
|
|
* @return 绝对区域
|
|
|
*/
|
|
|
public Location of(Location relativePoint) {
|
|
|
return of(relativePoint.getX(), relativePoint.getY());
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将相对坐标转为色值坐标
|
|
|
*
|
|
|
* @return 绝对坐标
|
|
|
*/
|
|
|
public ColorLocation of(int x, int y, String hexColor) {
|
|
|
int ox = getWorkRegion().getX();
|
|
|
int oy = getWorkRegion().getY();
|
|
|
return new ColorLocation(x + ox, y + oy, hexColor);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将相对坐标转为色值坐标
|
|
|
*
|
|
|
* @return 绝对坐标
|
|
|
*/
|
|
|
public String[] of(String... legends) {
|
|
|
return legends;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 将相对坐标转为色值坐标
|
|
|
*
|
|
|
* @param relativePoint 相对坐标
|
|
|
* @return 绝对区域
|
|
|
*/
|
|
|
public ColorLocation of(Location relativePoint, String hexColor) {
|
|
|
return of(relativePoint.getX(), relativePoint.getY(), hexColor);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 获取随机中心坐标
|
|
|
*
|
|
|
* @param region 区域
|
|
|
* @return 随机中心坐标
|
|
|
*/
|
|
|
public Location randomCenter(Region region) {
|
|
|
// 随机移动到区域中心附近(而非直接移动到中心)
|
|
|
Location center = region.getCenter();
|
|
|
// 得到随机移动到区域中心附近的半径
|
|
|
int radius = (int) (Math.min(region.getW(), region.getH()) / 2 * 0.9f);
|
|
|
int x = center.getX() + RandomUtil.randomInt(-radius, radius);
|
|
|
int y = center.getY() + RandomUtil.randomInt(-radius, radius);
|
|
|
return new Location(x, y);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 输入文本
|
|
|
*
|
|
|
* @param text 文本
|
|
|
*/
|
|
|
public void input(String text) {
|
|
|
// 纯英文输入以type输入,其他情况使用paste输入
|
|
|
if (text.matches("^[a-zA-Z0-9]+$")) {
|
|
|
for (char c : text.toCharArray()) {
|
|
|
try {
|
|
|
// 每个字符输入间隔随机(100-500 毫秒)
|
|
|
long delay = RandomUtil.randomLong(100, 500);
|
|
|
screen.type(String.valueOf(c));
|
|
|
Thread.sleep(delay);
|
|
|
} catch (InterruptedException e) {
|
|
|
throw new IllegalArgumentException(e);
|
|
|
}
|
|
|
}
|
|
|
workRegion.type(text);
|
|
|
} else {
|
|
|
workRegion.paste(text);
|
|
|
}
|
|
|
}
|
|
|
} |