阶段四收尾:面板两行布局(row/expand/fill)+ 英文输出 + 混淆名对照表

- ViewInstaller:用 Table.row()/Cell.expand()+fill()/setFillParent 排出"
  按钮一行、可滚动文字区一行"的面板;修掉把 setDebug 当成 setFillParent 的误用
- Scene2dPanel:按钮 isChecked/setChecked 与 Label.setText 改用对照表定名,
  修掉"点一次反复触发"与"文字不刷新"两个 bug
- SceneReader:面板输出改英文(编辑器缺中文字形,中文会渲染成方块)
- 新增 tools/obfmap/MapMethods.java:载荷 vs 上游 libGDX 的字节码指纹比对工具
- 新增 混淆名对照表.md:类/方法/字段映射、推导办法、已踩的坑、验证状态
This commit is contained in:
tianmo committed 2026-10-16 14:06:42 +08:00
1 parent 2e7ab7c33c
commit 8f7b72271f
8 files changed
+677 -52

No files matched your search

+71 -19
View File
@@ -156,24 +156,21 @@ public final class Scene2dPanel {
throws Exception {
Class<?> buttonClass = button.getClass();
Class<?> labelClass = label.getClass();
Method setChecked = null;
Method isChecked = null;
for (Method setter : buttonClass.getMethods()) {
if (setter.getParameterTypes().length != 1
|| setter.getParameterTypes()[0] != boolean.class
|| setter.getReturnType() != void.class) continue;
setter.invoke(button, Boolean.TRUE);
for (Method getter : buttonClass.getMethods()) {
if (getter.getParameterTypes().length != 0
|| getter.getReturnType() != boolean.class) continue;
if (Boolean.TRUE.equals(getter.invoke(button))) {
setChecked = setter;
isChecked = getter;
break;
}
// 优先用对照表里定好的名字:Button 上无参方法叫 "String" 的有两个(boolean 和 float),
// 只按名字取会取错,必须连返回类型一起判。
Method isChecked = findPinned(buttonClass, Obf.BUTTON_IS_CHECKED, boolean.class);
Method setChecked = findPinned(buttonClass, Obf.BUTTON_SET_CHECKED, void.class, boolean.class);
if (isChecked == null || setChecked == null) {
// 名字对不上(换版本)就退回运行期自校准 —— 只在 Button 自己这一层里试,
// 不然会扫到继承来的方法,选出一对语义无关的 setter/getter(现象是点击被反复触发)。
System.err.println("[panel] 对照表里的 isChecked/setChecked 没找到,改用自校准");
Method[] pair = calibrateChecked(button, buttonClass);
if (pair == null) {
System.err.println("[panel] 没能标定 isChecked/setChecked");
return;
}
setter.invoke(button, Boolean.FALSE);
if (isChecked != null) break;
isChecked = pair[0];
setChecked = pair[1];
}
if (isChecked == null) {
System.err.println("[panel] 没能标定 isChecked/setChecked");
@@ -184,7 +181,17 @@ public final class Scene2dPanel {
final Method readChecked = isChecked;
final Method resetChecked = setChecked;
final Method setText = findMethod(labelClass, 1, CharSequence.class);
// setChecked(boolean, boolean) 的第二个参数是"是否派发事件"。复位时用 false,
// 否则把 isChecked 置回 false 这件事本身又会派发一次事件,表现为按一下读两次。
final Method resetQuietly = findPinned(buttonClass, Obf.BUTTON_SET_CHECKED_FIRE, void.class,
boolean.class, boolean.class);
// 必须连返回类型一起判:Label 上 `D(CharSequence)` 有 void(setText)和 boolean
// (textEquals)两个,按参数个数找会挑中 textEquals,表现是文字永远不刷新。
final Method setText = findPinned(labelClass, Obf.LABEL_SET_TEXT, void.class, CharSequence.class);
if (setText == null) {
System.err.println("[panel] 没找到 Label.setText,面板文字不会更新");
return;
}
final ClassLoader classLoader = loader;
Thread poll = new Thread(new Runnable() {
@Override
@@ -192,7 +199,11 @@ public final class Scene2dPanel {
while (true) {
try {
if (Boolean.TRUE.equals(readChecked.invoke(button))) {
resetChecked.invoke(button, Boolean.FALSE);
if (resetQuietly != null) {
resetQuietly.invoke(button, Boolean.FALSE, Boolean.FALSE);
} else {
resetChecked.invoke(button, Boolean.FALSE);
}
System.err.println("[panel] 按钮被触发,开始读取场景对象");
// Actor 自己记着所在 Stage,直接问它就行
Object stage = firstFieldOfType(button, STAGE);
@@ -229,6 +240,47 @@ public final class Scene2dPanel {
}
}
/** 按"名字 + 参数类型 + 返回类型"找方法:混淆后同名方法成堆,少判一个条件就会挑错。 */
private static Method findPinned(Class<?> owner, String name, Class<?> returnType,
Class<?>... parameterTypes) {
for (Method method : owner.getMethods()) {
if (!method.getName().equals(name)) continue;
if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
if (returnType != null && method.getReturnType() != returnType) continue;
return method;
}
return null;
}
/**
* 运行期自校准:把每个 boolean setter 设成 true,看哪个无参 boolean getter 跟着变 true。
* 只在按钮类自己这一层(含父类)的方法里找 —— 扫到继承链上无关的方法会配出一对假组合,
* 现象是"点一次按钮被反复触发"。
*/
private static Method[] calibrateChecked(Object button, Class<?> buttonClass) throws Exception {
for (Class<?> type = buttonClass; type != null && type != Object.class;
type = type.getSuperclass()) {
for (Method setter : type.getDeclaredMethods()) {
if (setter.getParameterTypes().length != 1
|| setter.getParameterTypes()[0] != boolean.class
|| setter.getReturnType() != void.class) continue;
setter.setAccessible(true);
setter.invoke(button, Boolean.TRUE);
for (Method getter : type.getDeclaredMethods()) {
if (getter.getParameterTypes().length != 0
|| getter.getReturnType() != boolean.class) continue;
getter.setAccessible(true);
if (Boolean.TRUE.equals(getter.invoke(button))) {
setter.invoke(button, Boolean.FALSE);
return new Method[] {getter, setter};
}
}
setter.invoke(button, Boolean.FALSE);
}
}
return null;
}
/** 从 Fx.D(静态 Actor)出发拿 Stage:libGDX 里每个 Actor 都记着自己所在的 Stage。 */
private static Object stageFromActorAnchor(ClassLoader loader) {
try {