AutoStore 模组完整源码
这个提交包含在:
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152),
|
||||
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
|
||||
*/
|
||||
package autostore.client.monitor;
|
||||
|
||||
import autostore.client.territory.ChunkPos;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
|
||||
@Environment(value=EnvType.CLIENT)
|
||||
/**
|
||||
* 字幕/聊天监听器:解析游戏内消息,识别开垦进度与结果。
|
||||
*
|
||||
* <p>两条输入通道:
|
||||
* <ul>
|
||||
* <li>{@link #onSubtitle}:来自 HUD 字幕(如"开垦中 50%"),提取百分比进度</li>
|
||||
* <li>{@link #onChat}:来自聊天栏(如"疆土(x,z) [bx,bz] 已为你的邦国开拓"),提取成功结果</li>
|
||||
* </ul>
|
||||
* 消息匹配采用模糊匹配(Levenshtein 距离),以兼容服务器端文案的细微差异。
|
||||
*/
|
||||
public final class SubtitleChatMonitor {
|
||||
/** 匹配字幕中的百分比:如 "开垦中 50%" */
|
||||
private static final Pattern PERCENT_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*%");
|
||||
/** 匹配开垦成功的聊天消息:形如 "疆土(区块X,区块Z) [方块X,方块Z] 已为你的邦国开拓" */
|
||||
private static final Pattern CLAIM_MESSAGE_PATTERN = Pattern.compile("\u7586\u571f[\uff08(]\\s*(-?\\d+)\\s*[\uff0c,]\\s*(-?\\d+)\\s*[)\uff09]\\s*\\[\\s*(-?\\d+)\\s*[\uff0c,]\\s*(-?\\d+)\\s*]\\s*\u5df2\u4e3a\u4f60\u7684\u90a6\u56fd\u5f00\u62d3");
|
||||
/** 结果回调监听器 */
|
||||
private final Listener listener;
|
||||
|
||||
public SubtitleChatMonitor(Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理字幕消息:若包含"开垦"相关文本,则提取其中的百分比并回调进度。
|
||||
* 使用模糊匹配容忍服务器文案的细微差异。
|
||||
*/
|
||||
public void onSubtitle(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!SubtitleChatMonitor.containsFuzzy(text, "\u5f00\u62d3\u4e2d", 2) && !SubtitleChatMonitor.containsFuzzy(text, "\u5f00\u62d3", 1)) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = PERCENT_PATTERN.matcher(text);
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
int percent = (int)Math.round(Double.parseDouble(matcher.group(1)));
|
||||
this.listener.onProgress(Math.max(0, Math.min(100, percent)));
|
||||
}
|
||||
catch (NumberFormatException numberFormatException) {
|
||||
// 百分比解析失败时忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理聊天消息:优先匹配带坐标的开垦成功消息(回调 onSuccess),
|
||||
* 否则模糊匹配"疆土已开拓/已为你的邦国开拓"等通用成功文案(回调 onGenericSuccess)。
|
||||
*/
|
||||
public void onChat(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = CLAIM_MESSAGE_PATTERN.matcher(text);
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
ChunkPos chunk = new ChunkPos(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
|
||||
int blockX = Integer.parseInt(matcher.group(3));
|
||||
int blockZ = Integer.parseInt(matcher.group(4));
|
||||
this.listener.onSuccess(chunk, blockX, blockZ);
|
||||
return;
|
||||
}
|
||||
catch (NumberFormatException numberFormatException) {
|
||||
// 坐标解析失败时忽略,尝试匹配通用消息
|
||||
}
|
||||
}
|
||||
if (SubtitleChatMonitor.containsFuzzy(text, "\u7586\u571f\u5df2\u5f00\u62d3", 3) || SubtitleChatMonitor.containsFuzzy(text, "\u5df2\u4e3a\u4f60\u7684\u90a6\u56fd\u5f00\u62d3", 4)) {
|
||||
this.listener.onGenericSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模糊包含匹配:判断 text 中是否存在与 needle 编辑距离 ≤ maxEdits 的子串。
|
||||
* 采用滑动窗口 + 有界 Levenshtein 距离,用于容忍服务器文案的细微差异。
|
||||
*/
|
||||
public static boolean containsFuzzy(String text, String needle, int maxEdits) {
|
||||
if (needle == null || needle.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
int n = text.length();
|
||||
int m = needle.length();
|
||||
if (m > n + maxEdits) {
|
||||
return false;
|
||||
}
|
||||
boolean start = false;
|
||||
int end = Math.min(n, m + maxEdits);
|
||||
int best = maxEdits + 1;
|
||||
for (int i = 0; i <= n; ++i) {
|
||||
int from = Math.max(0, i - maxEdits);
|
||||
int to = Math.min(n, i + m + maxEdits);
|
||||
if (to - from < m) continue;
|
||||
int j = from;
|
||||
while (j + m <= to) {
|
||||
int distance = SubtitleChatMonitor.boundedLevenshtein(text, j, needle, maxEdits + 1);
|
||||
if (distance < best) {
|
||||
best = distance;
|
||||
}
|
||||
if (best == 0) {
|
||||
return true;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
}
|
||||
return best <= maxEdits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 有界 Levenshtein 距离:计算 text 从 textOffset 起的子串与 needle 的编辑距离。
|
||||
* 当某行最小距离超过 limit 时提前返回(剪枝优化),避免全量计算。
|
||||
*/
|
||||
private static int boundedLevenshtein(String text, int textOffset, String needle, int limit) {
|
||||
int m = needle.length();
|
||||
int[] previous = new int[m + 1];
|
||||
int[] current = new int[m + 1];
|
||||
for (int j = 0; j <= m; ++j) {
|
||||
previous[j] = j;
|
||||
}
|
||||
int maxI = Math.min(m + limit, text.length() - textOffset);
|
||||
for (int i = 1; i <= maxI; ++i) {
|
||||
current[0] = i;
|
||||
int rowMin = current[0];
|
||||
for (int j = 1; j <= m; ++j) {
|
||||
int cost = text.charAt(textOffset + i - 1) == needle.charAt(j - 1) ? 0 : 1;
|
||||
current[j] = Math.min(Math.min(current[j - 1] + 1, previous[j] + 1), previous[j - 1] + cost);
|
||||
rowMin = Math.min(rowMin, current[j]);
|
||||
}
|
||||
if (rowMin > limit) {
|
||||
return limit + 1;
|
||||
}
|
||||
int[] swap = previous;
|
||||
previous = current;
|
||||
current = swap;
|
||||
}
|
||||
return previous[m];
|
||||
}
|
||||
|
||||
/** 扩地结果回调接口:进度更新、开垦成功(带坐标)、开垦成功(通用) */
|
||||
@Environment(value=EnvType.CLIENT)
|
||||
public static interface Listener {
|
||||
public void onProgress(int var1);
|
||||
|
||||
public void onSuccess(ChunkPos var1, int var2, int var3);
|
||||
|
||||
public void onGenericSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户