Skip to content

Commit 5fdf817

Browse files
committed
Compile dropped-item label templates
1 parent 9756600 commit 5fdf817

3 files changed

Lines changed: 344 additions & 47 deletions

File tree

common/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemDisplay.java

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,10 @@
2222
import com.loohp.interactionvisualizer.objectholders.EntryKey;
2323
import com.loohp.interactionvisualizer.utils.ChatColorUtils;
2424
import com.loohp.interactionvisualizer.utils.ItemNameUtils;
25-
import com.loohp.interactionvisualizer.utils.LegacyTextComponentCache;
2625
import com.loohp.interactionvisualizer.scheduler.ScheduledRunnable;
2726
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
2827
import com.loohp.interactionvisualizer.scheduler.Scheduler;
2928
import net.kyori.adventure.text.Component;
30-
import net.kyori.adventure.text.TextReplacementConfig;
3129
import net.kyori.adventure.text.format.NamedTextColor;
3230
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
3331
import org.bukkit.Bukkit;
@@ -93,12 +91,10 @@ public final class DroppedItemDisplay extends VisualizerRunnableDisplay implemen
9391
private final DroppedItemSpatialIndex crampIndex = new DroppedItemSpatialIndex();
9492
private final NamespacedKey visualEntityKey;
9593

96-
private String regularFormatting;
97-
private String singularFormatting;
98-
private String toolsFormatting;
9994
private String highColor = "";
10095
private String mediumColor = "";
10196
private String lowColor = "";
97+
private DroppedItemLabelFormatter labelFormatter;
10298
private int cramp = 6;
10399
private double labelYOffset = 0.8D;
104100
private int updateRate = 20;
@@ -119,12 +115,15 @@ public DroppedItemDisplay() {
119115
@EventHandler
120116
public void onReload(InteractionVisualizerReloadEvent event) {
121117
DroppedItemVisibilityPolicy previousVisibilityPolicy = visibilityPolicy;
122-
regularFormatting = configString("Entities.Item.Options.RegularFormat");
123-
singularFormatting = configString("Entities.Item.Options.SingularFormat");
124-
toolsFormatting = configString("Entities.Item.Options.ToolsFormat");
118+
String regularFormatting = configString("Entities.Item.Options.RegularFormat");
119+
String singularFormatting = configString("Entities.Item.Options.SingularFormat");
120+
String toolsFormatting = configString("Entities.Item.Options.ToolsFormat");
125121
highColor = configString("Entities.Item.Options.Color.High");
126122
mediumColor = configString("Entities.Item.Options.Color.Medium");
127123
lowColor = configString("Entities.Item.Options.Color.Low");
124+
labelFormatter = new DroppedItemLabelFormatter(
125+
regularFormatting, singularFormatting, toolsFormatting,
126+
highColor, mediumColor, lowColor);
128127
cramp = InteractionVisualizer.plugin.getConfiguration().getInt("Entities.Item.Options.Cramping");
129128
double configuredLabelYOffset = InteractionVisualizer.plugin.getConfiguration()
130129
.getDouble("Entities.Item.Options.LabelYOffset");
@@ -577,16 +576,22 @@ private void update(UUID itemId, Item item, DroppedItemSpatialIndex itemIndex) {
577576
return;
578577
}
579578

580-
Component text = format(content, ticksLeft);
579+
Component text = labelFormatter.format(content.formatState, ticksLeft);
581580
boolean created = false;
582581
if (label == null || !label.isValid() || !label.getWorld().equals(item.getWorld())) {
583582
removeLabel(itemId);
584583
label = spawnLabel(item);
585584
labels.put(itemId, label);
586585
created = true;
587586
}
588-
if (!text.equals(label.text())) {
587+
// The formatter returns the same immutable component instance while
588+
// the rendered state is unchanged. Avoid a CraftTextDisplay read on
589+
// every refresh; it serializes the NMS component back through Paper.
590+
int labelId = label.getEntityId();
591+
if (content.appliedLabelId != labelId || content.appliedText != text) {
589592
label.text(text);
593+
content.appliedLabelId = labelId;
594+
content.appliedText = text;
590595
}
591596
float targetViewRange = labelViewRange();
592597
if (Math.abs(label.getViewRange() - targetViewRange) > 1.0E-4F) {
@@ -912,42 +917,16 @@ private CachedItemContent cachedContent(UUID itemId, ItemStack stack) {
912917
NamespacedKey customItemId = blacklist.requiresCustomItemId()
913918
? CustomContentManager.customItemId(stack).orElse(null)
914919
: null;
920+
String durability = durability(stack);
921+
Component itemName = ItemNameUtils.getDisplayName(stack);
915922
CachedItemContent replacement = new CachedItemContent(
916923
stack.clone(),
917924
blacklist.matches(matchingName, stack.getType(), customItemId),
918-
durability(stack),
919-
ItemNameUtils.getDisplayName(stack));
925+
labelFormatter.state(stack.getAmount(), durability, itemName));
920926
contentCache.put(itemId, replacement);
921927
return replacement;
922928
}
923929

924-
private Component format(CachedItemContent content, int ticksLeft) {
925-
int secondsLeft = Math.max(0, ticksLeft / 20);
926-
if (content.lastSeconds == secondsLeft && content.lastFormatted != null) {
927-
return content.lastFormatted;
928-
}
929-
ItemStack stack = content.stack;
930-
int amount = stack.getAmount();
931-
String timerColor = secondsLeft <= 30 ? lowColor : secondsLeft <= 120 ? mediumColor : highColor;
932-
String timer = timerColor + String.format(java.util.Locale.ROOT, "%02d:%02d", secondsLeft / 60, secondsLeft % 60);
933-
934-
String template;
935-
if (ticksLeft >= 600 && content.durability != null) {
936-
template = toolsFormatting.replace("{Durability}", content.durability);
937-
} else {
938-
template = amount == 1 ? singularFormatting : regularFormatting;
939-
}
940-
String rendered = template.replace("{Amount}", Integer.toString(amount)).replace("{Timer}", timer);
941-
Component component = LegacyTextComponentCache.parse(rendered);
942-
Component formatted = component.replaceText(TextReplacementConfig.builder()
943-
.matchLiteral("{Item}")
944-
.replacement(content.itemName)
945-
.build());
946-
content.lastSeconds = secondsLeft;
947-
content.lastFormatted = formatted;
948-
return formatted;
949-
}
950-
951930
private String durability(ItemStack stack) {
952931
if (stack.getType().getMaxDurability() <= 0 || !(stack.getItemMeta() instanceof Damageable damageable)) {
953932
return null;
@@ -1055,17 +1034,15 @@ private static final class CachedItemContent {
10551034

10561035
private final ItemStack stack;
10571036
private final boolean blacklisted;
1058-
private final String durability;
1059-
private final Component itemName;
1060-
private int lastSeconds = Integer.MIN_VALUE;
1061-
private Component lastFormatted;
1037+
private final DroppedItemLabelFormatter.State formatState;
1038+
private int appliedLabelId = Integer.MIN_VALUE;
1039+
private Component appliedText;
10621040

1063-
private CachedItemContent(ItemStack stack, boolean blacklisted, String durability,
1064-
Component itemName) {
1041+
private CachedItemContent(ItemStack stack, boolean blacklisted,
1042+
DroppedItemLabelFormatter.State formatState) {
10651043
this.stack = stack;
10661044
this.blacklisted = blacklisted;
1067-
this.durability = durability;
1068-
this.itemName = itemName;
1045+
this.formatState = formatState;
10691046
}
10701047
}
10711048

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* This file is part of InteractionVisualizer.
3+
*
4+
* Copyright (C) 2026. Contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*/
11+
12+
package com.loohp.interactionvisualizer.entities;
13+
14+
import com.loohp.interactionvisualizer.utils.LegacyTextComponentCache;
15+
import net.kyori.adventure.text.Component;
16+
import net.kyori.adventure.text.TextReplacementConfig;
17+
18+
import java.util.LinkedHashMap;
19+
import java.util.Map;
20+
import java.util.Objects;
21+
22+
/** Reload-scoped compiler for dropped-item label templates. */
23+
final class DroppedItemLabelFormatter {
24+
25+
private static final String TIMER_TOKEN = "{IV_INTERNAL_TIMER_6D42D50B}";
26+
private static final int MAX_SHARED_TEMPLATES = 512;
27+
private static final int REGULAR_VARIANT = 0;
28+
private static final int SINGULAR_VARIANT = 1;
29+
private static final int TOOLS_VARIANT = 2;
30+
31+
private final String regularTemplate;
32+
private final String singularTemplate;
33+
private final String toolsTemplate;
34+
private final String highColor;
35+
private final String mediumColor;
36+
private final String lowColor;
37+
private final Map<String, Component> sharedTemplates = new LinkedHashMap<>(64, 0.75F, true) {
38+
@Override
39+
protected boolean removeEldestEntry(Map.Entry<String, Component> eldest) {
40+
return size() > MAX_SHARED_TEMPLATES;
41+
}
42+
};
43+
44+
DroppedItemLabelFormatter(String regularTemplate, String singularTemplate,
45+
String toolsTemplate, String highColor,
46+
String mediumColor, String lowColor) {
47+
this.regularTemplate = Objects.requireNonNull(regularTemplate, "regularTemplate");
48+
this.singularTemplate = Objects.requireNonNull(singularTemplate, "singularTemplate");
49+
this.toolsTemplate = Objects.requireNonNull(toolsTemplate, "toolsTemplate");
50+
this.highColor = Objects.requireNonNull(highColor, "highColor");
51+
this.mediumColor = Objects.requireNonNull(mediumColor, "mediumColor");
52+
this.lowColor = Objects.requireNonNull(lowColor, "lowColor");
53+
}
54+
55+
State state(int amount, String durability, Component itemName) {
56+
return new State(amount, durability, Objects.requireNonNull(itemName, "itemName"));
57+
}
58+
59+
int sharedTemplateCount() {
60+
return sharedTemplates.size();
61+
}
62+
63+
Component format(State state, int ticksLeft) {
64+
int secondsLeft = Math.max(0, ticksLeft / 20);
65+
int variant = ticksLeft >= 600 && state.durability != null
66+
? TOOLS_VARIANT : state.amount == 1 ? SINGULAR_VARIANT : REGULAR_VARIANT;
67+
String template = switch (variant) {
68+
case TOOLS_VARIANT -> toolsTemplate;
69+
case SINGULAR_VARIANT -> singularTemplate;
70+
default -> regularTemplate;
71+
};
72+
boolean hasTimer = template.contains("{Timer}");
73+
String timerColor = hasTimer ? timerColor(secondsLeft) : null;
74+
if (state.compiled == null || state.compiledVariant != variant
75+
|| !Objects.equals(state.compiledTimerColor, timerColor)) {
76+
compile(state, variant, template, timerColor, hasTimer);
77+
}
78+
if (!state.compiledHasTimer) {
79+
return state.compiled;
80+
}
81+
if (state.lastSeconds == secondsLeft && state.lastFormatted != null) {
82+
return state.lastFormatted;
83+
}
84+
String timerText = timerText(secondsLeft);
85+
Component formatted = state.compiled.replaceText(TextReplacementConfig.builder()
86+
.matchLiteral(TIMER_TOKEN)
87+
.replacement(builder -> builder.content(timerText))
88+
.build()).compact();
89+
state.lastSeconds = secondsLeft;
90+
state.lastFormatted = formatted;
91+
return formatted;
92+
}
93+
94+
private void compile(State state, int variant, String template,
95+
String timerColor, boolean hasTimer) {
96+
String raw = template;
97+
if (variant == TOOLS_VARIANT) {
98+
raw = raw.replace("{Durability}", state.durability);
99+
}
100+
raw = raw.replace("{Amount}", Integer.toString(state.amount));
101+
if (hasTimer) {
102+
raw = raw.replace("{Timer}", timerColor + TIMER_TOKEN);
103+
}
104+
Component parsed = sharedTemplates.computeIfAbsent(raw, LegacyTextComponentCache::parse);
105+
state.compiled = parsed.replaceText(TextReplacementConfig.builder()
106+
.matchLiteral("{Item}")
107+
.replacement(state.itemName)
108+
.build());
109+
state.compiledVariant = variant;
110+
state.compiledTimerColor = timerColor;
111+
state.compiledHasTimer = hasTimer;
112+
state.lastSeconds = Integer.MIN_VALUE;
113+
state.lastFormatted = null;
114+
}
115+
116+
private String timerColor(int secondsLeft) {
117+
return secondsLeft <= 30 ? lowColor : secondsLeft <= 120 ? mediumColor : highColor;
118+
}
119+
120+
static String timerText(int secondsLeft) {
121+
int minutes = secondsLeft / 60;
122+
int seconds = secondsLeft % 60;
123+
String minuteText = minutes < 10 ? "0" + minutes : Integer.toString(minutes);
124+
String secondText = seconds < 10 ? "0" + seconds : Integer.toString(seconds);
125+
return minuteText + ":" + secondText;
126+
}
127+
128+
static final class State {
129+
130+
private final int amount;
131+
private final String durability;
132+
private final Component itemName;
133+
private int compiledVariant = Integer.MIN_VALUE;
134+
private String compiledTimerColor;
135+
private boolean compiledHasTimer;
136+
private Component compiled;
137+
private int lastSeconds = Integer.MIN_VALUE;
138+
private Component lastFormatted;
139+
140+
private State(int amount, String durability, Component itemName) {
141+
this.amount = amount;
142+
this.durability = durability;
143+
this.itemName = itemName;
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)