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 "";

三段式:

  1. isValidSize() 为 false → 返回空串(该 value 被完全跳过)
  2. 求值抛异常 → "<ERROR>"
  3. 正常 → 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|...} 的四步处理:

  1. 找 {ICON| 的位置
  2. 找它后面的 |,加 1 得到宽度参数起点
  3. 用 getStringWidth(前缀) 算出图标该放的像素 X
  4. 从 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, 所以调试信息里看不到实际挂载的图标。

相关