InfoText
一行文本:拼接 value、挂载图标 / 物品
基本信息
| 属性 | 值 |
|---|---|
| 路径 | com.github.lunatrius.ingameinfo.client.gui.InfoText |
| 行数 | 139 |
| 父类 | Info |
| 角色 | 渲染树的根节点(9 个对齐各持一组 InfoText) |
字段(:18-24)
private static final String ICON_START = "{ICON|"; // :18
private final Map<String, Info> attachedValues = new LinkedHashMap<>(); // :19
private String text; // :20
private final List<Value> values; // :21
private final Alignment alignment; // :22
private int index; // :23
private boolean isActive; // :24
⚠️ 两套子节点容器并存:
父类 Info 的 children(:14,本类不使用)
与本类的 attachedValues(:19,实际使用)。
draw()(继承自 Info)遍历 children(空),
而 drawInfo()(:57-61)自己遍历 attachedValues 并调 child.draw()。
所以挂载走的是 attachedValues,children 永远是空的。
⚠️ attachedValues 用 LinkedHashMap ——
保证图标按挂载顺序绘制,这是「按文本中 {ICON|...} 出现位置」定位的前提。
构造器(:26-33)
public InfoText(Alignment alignment, List<Value> values) {
super(0, 0);
this.values = values;
this.alignment = alignment;
for (Value value : values) { value.setParent(this); } // :30-32 递归注入 parent
}
setParent 是 Value 的方法(:143-148),
递归给所有子节点设置同一 parent ——
ValueComplex 的 ValueIcon 靠它拿到 InfoText。
由 InGameInfoCore.refreshInfoTexts(:226)创建:
new InfoText(alignment, line)。
update(:35-46)—— 每 tick 调用
this.index = index;
StringBuilder builder = new StringBuilder();
for (Value value : this.values) { builder.append(getValue(value)); }
isActive = builder.length() > 0; // :42
updateChildren(builder);
text = builder.toString();
updatePosition();
| 步骤 | 说明 |
|---|---|
记录 index |
同对齐内的行序号,从 0 递增 |
| 逐 value 拼接 | getValue(value) 见下 |
isActive = 长度 > 0 |
空行被标记为不活跃 |
updateChildren |
剥离 {ICON|...} 标记并定位图标 |
text 定稿 |
剥离后的纯文本 |
updatePosition |
算 x / y |
getValue(:116-126)
try {
if (value.isValidSize()) { return value.getReplacedValue(); }
} catch (Exception e) { return "<ERROR>"; }
return "";
三段式:
isValidSize()为 false → 返回空串(该 value 被完全跳过)- 求值抛异常 →
"<ERROR>" - 正常 →
getReplacedValue()(做{tag}变量替换)
⚠️ "<ERROR>" 是字面量文本,会真的显示在 HUD 上。
配置里 tag 名写错、value 参数个数不对、tag 返回非数字等
都会导致屏幕上出现 <ERROR> 而非静默失败。
isActive 的语义
isActive 只看拼接后长度,所以:
- 所有 value 都返回空串 → 整行不画
- 任一 value 返回非空 → 整行画(包括
<ERROR>)
InGameInfoCore.onTickClient(:145-152)
用 lastActiveIndex 递增来给活跃行编号 ——
空行不占号,所以下面的行会自动上移。
这是 index 的来源。
drawInfo(:52-62)
if (!isActive) return;
fontRenderer.drawStringWithShadow(text, getX(), getY(), 0x00FFFFFF); // :55
for (Info child : attachedValues.values()) {
child.offsetX = x; child.offsetY = y; child.draw();
}
⚠️ 文本颜色是硬编码 0x00FFFFFF(纯白),
不跟随任何配置或格式码状态。
默认配置里的 $e / $a 等是通过 Operation
写进 text 字符串的(格式码),不是改这个颜色参数。
⚠️ drawStringWithShadow 带阴影 ——
即使 text 里已有 §r 重置,阴影仍会画。
updateChildren(:64-82)—— 图标定位算法
if (builder.length() == 0 && !attachedValues.isEmpty()) { attachedValues.clear(); return; } // :65-68
Iterator<Info> iter = attachedValues.values().iterator();
while (builder.indexOf(ICON_START) >= 0 && iter.hasNext()) { // :72
Info child = iter.next();
if (child.hasPosition) continue; // :74 已定位 → 跳过
int iconStart = builder.indexOf(ICON_START);
int widthStart = builder.indexOf("|", iconStart) + 1; // :76
child.hasPosition = true; // :77
child.x = fontRenderer.getStringWidth(builder.substring(0, iconStart)); // :78
builder.replace(iconStart, widthStart, ""); // :79 删掉 {ICON|xxx
builder.deleteCharAt(builder.indexOf("}")); // :80 删掉 }
}
{ICON|...} 的四步处理:
- 找
{ICON|的位置 - 找它后面的
|,加 1 得到宽度参数起点 - 用
getStringWidth(前缀)算出图标该放的像素 X - 从
builder里删掉{ICON|+ 宽度参数 +}
三个缺陷
⚠️ continue 不推进迭代器也不改文本(:74):
已定位的子节点被跳过,但 builder 里的 {ICON|...} 不删。
下一轮循环的 builder.indexOf(ICON_START) 仍找到同一个标记,
而 iter.next() 已推进到下一个子节点 ——
子节点与标记的对应关系会错位。
正常情况下不该触发(hasPosition 初值 false,
每次 update 都会重新定位),但 TagPlayerPotion
的图标在药水 ID 变化时会 removeAttachedValue + 重新 attachValue
(新实例 hasPosition 为 false),此时若旧实例仍在 attachedValues 里就会错位。
⚠️ attachedValues 只增不减(除 :65-68 的整体清空):
update 每 tick 跑一次,图标子节点只被 hasPosition 标记,
不会重复 attach。所以正常路径稳定。
⚠️ 整个循环只在 builder.length() > 0 时有意义:
若文本为空但有挂载值,:65-68 清空全部挂载。
与 Value.replaceVariables 的早退呼应
Value 的 replaceVariables(:55)有
tagAmount == 1 && str.contains("ICON") 的早退,
就是为了不让 {ICON|...} 被当作 tag 变量替换掉。
两处是同一语法的两个处理点。
updatePosition(:84-89)
x = alignment.getX(scaledWidth, fontRenderer.getStringWidth(text));
y = alignment.getY(scaledHeight, getHeight());
text 已剥离 {ICON|...},
所以图标不占文本宽度 —— 图标覆盖在文本之上而非推挤文本。
⚠️ scaledWidth / scaledHeight 来自
InGameInfoCore(:136-137),
已按 Scale / 10 缩放过。
getHeight 的上下半屏差异(:108-114)
if (alignment.ordinal() >= Alignment.BOTTOMLEFT.ordinal()) {
return (index + 1) * (fontRenderer.FONT_HEIGHT + 1);
}
return index * (fontRenderer.FONT_HEIGHT + 1);
| 区域 | 公式 |
|---|---|
| 上/中部(ordinal < 6) | index * (FONT_HEIGHT + 1) |
| 底部(ordinal ≥ 6) | (index + 1) * (FONT_HEIGHT + 1) |
⚠️ 用 ordinal() 做比较——依赖 Alignment
的枚举声明顺序(BOTTOMLEFT 是第 7 个,ordinal 6)。
若有人在 MIDDLERIGHT 之后插入新常量,这个分界会移位。
底部三行用 index + 1 是因为
Alignment.getY 的 BOTTOM 分支是
y + screenheight - textheight,
需要额外一行高度才能把首行抬离屏幕底边 ——
这与 BOTTOMCENTER 默认 Y = -45 是同一套避让逻辑。
⚠️ getWidth()(:103-106)用 getStringWidth(text),
不含图标 —— 图标是覆盖的。
三个访问器
| 方法 | 行 | 说明 |
|---|---|---|
getAttachedValue(String tag) |
:91-93 |
返回 @Nullable Info |
removeAttachedValue(String tag) |
:95-97 |
按 key 移除 |
attachValue(@NotNull String tag, @NotNull Info value) |
:99-101 |
按 key 挂载 |
按 tag 名为 key,所以同一行有两个同名 tag 时后者覆盖前者。
toString(:128-138)
InfoText{text: %s, x: %d, y: %d, offsetX: %d, offsetY: %d, children: %s}
—— 输出 children(父类字段,恒为空)而非 attachedValues,
所以调试信息里看不到实际挂载的图标。
相关
- Info - 父类
- Value -
setParent/getReplacedValue - Alignment -
getX/getY与 ordinal 分界 - TagPlayerPotion -
removeAttachedValue/attachValue的实际调用方 - InGameInfoCore - 创建者与每 tick 调用者