Skip to content

Commit d5d2a52

Browse files
committed
feat: Add complete HUD monitoring and live preview system
Implement end-to-end HUD element support with real-time text capture: Backend: - Add mixin-based HUD rendering interception (HudMixin, HudRendererMixin) - Create HudPreviewCapture for text extraction with non-text marker detection - Build HudPreviewService for off-thread snapshot diffing and broadcasting - Implement HudMapper for metadata, settings, and state serialization - Extend WebSocket protocol with hud.preview.update, hud.state.changed, hud.setting.value.changed message types - Add EventMonitor hooks for HUD element state and setting changes - Fix mixin crash by using safe @reDIrect instead of local-capturing @Inject - Use unique HUD IDs (hud::name#hash) to prevent toggle spam Frontend: - Add dedicated HUD store with WebSocket handlers for real-time sync - Create HUD dashboard with live text previews - Implement HUD settings dialog with full setting support - Wire toolbar density toggle to switch between comfortable/compact cards - Add hide-decorated filter for text-output HUDs - Clean up Vite build output by removing old hashed bundles Misc: - Add BrowserHelper utility for opening URLs - Add chip-small style variant - Refactor ModuleCard layout for better badge positioning
1 parent 75cfd2b commit d5d2a52

26 files changed

Lines changed: 1727 additions & 106 deletions

build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ tasks {
115115
description = "Copy built WebUI files to resources"
116116
dependsOn("buildWebUI")
117117

118+
doFirst {
119+
project.delete("src/main/resources/webui")
120+
}
121+
118122
from("webui/dist")
119123
into("src/main/resources/webui")
120124
}

src/main/java/com/cope/meteorwebgui/MeteorWebGUIAddon.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.cope.meteorwebgui.events.EventMonitor;
44
import com.cope.meteorwebgui.gui.WebGUITab;
5+
import com.cope.meteorwebgui.hud.HudPreviewService;
56
import com.cope.meteorwebgui.server.MeteorWebServer;
67
import com.cope.meteorwebgui.systems.WebGUIConfig;
78
import meteordevelopment.meteorclient.MeteorClient;
@@ -25,6 +26,7 @@ public class MeteorWebGUIAddon extends MeteorAddon {
2526

2627
private static MeteorWebServer server;
2728
private static EventMonitor eventMonitor;
29+
private static HudPreviewService hudPreviewService;
2830

2931
@Override
3032
public void onInitialize() {
@@ -85,6 +87,9 @@ public static void startServer() {
8587
MeteorClient.EVENT_BUS.subscribe(eventMonitor);
8688
eventMonitor.startMonitoring();
8789

90+
hudPreviewService = new HudPreviewService(server);
91+
hudPreviewService.start();
92+
8893
LOG.info("WebGUI server started successfully");
8994
LOG.info("Access the WebGUI at: http://{}:{}", host, port);
9095

@@ -112,6 +117,11 @@ public static void stopServer() {
112117
eventMonitor = null;
113118
}
114119

120+
if (hudPreviewService != null) {
121+
hudPreviewService.stop();
122+
hudPreviewService = null;
123+
}
124+
115125
// Stop server
116126
server.shutdown();
117127
server = null;

src/main/java/com/cope/meteorwebgui/events/EventMonitor.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package com.cope.meteorwebgui.events;
22

3+
import com.cope.meteorwebgui.mapping.HudMapper;
34
import com.cope.meteorwebgui.server.MeteorWebServer;
45
import meteordevelopment.meteorclient.events.meteor.ActiveModulesChangedEvent;
6+
import meteordevelopment.meteorclient.events.render.Render2DEvent;
57
import meteordevelopment.meteorclient.settings.Setting;
68
import meteordevelopment.meteorclient.settings.SettingGroup;
9+
import meteordevelopment.meteorclient.systems.hud.Hud;
10+
import meteordevelopment.meteorclient.systems.hud.HudElement;
711
import meteordevelopment.meteorclient.systems.modules.Module;
812
import meteordevelopment.meteorclient.systems.modules.Modules;
913
import meteordevelopment.orbit.EventHandler;
@@ -23,6 +27,7 @@ public class EventMonitor {
2327
private final MeteorWebServer server;
2428
private final Map<Setting<?>, Consumer<?>> originalCallbacks = new HashMap<>();
2529
private final Map<String, Boolean> moduleStates = new HashMap<>();
30+
private final Map<String, Boolean> hudStates = new HashMap<>();
2631

2732
public EventMonitor(MeteorWebServer server) {
2833
this.server = server;
@@ -40,6 +45,8 @@ public void startMonitoring() {
4045
monitorModuleSettings(module);
4146
}
4247

48+
monitorHudElements();
49+
4350
LOG.info("Event monitoring started for {} modules", Modules.get().getCount());
4451
}
4552

@@ -67,6 +74,23 @@ private void onModuleToggle(ActiveModulesChangedEvent event) {
6774
}
6875
}
6976

77+
@EventHandler
78+
private void onHudRender(Render2DEvent event) {
79+
for (HudElement element : Hud.get()) {
80+
String id = HudMapper.getElementIdentifier(element);
81+
boolean currentState = element.isActive();
82+
Boolean previousState = hudStates.get(id);
83+
84+
if (previousState == null || previousState != currentState) {
85+
hudStates.put(id, currentState);
86+
if (server.isRunning()) {
87+
server.broadcastHudStateChange(element);
88+
LOG.debug("HUD state changed: {} -> {}", id, currentState);
89+
}
90+
}
91+
}
92+
}
93+
7094
/**
7195
* Wrap setting callbacks
7296
*/
@@ -78,6 +102,22 @@ private void monitorModuleSettings(Module module) {
78102
}
79103
}
80104

105+
private void monitorHudElements() {
106+
for (HudElement element : Hud.get()) {
107+
String id = HudMapper.getElementIdentifier(element);
108+
hudStates.put(id, element.isActive());
109+
monitorHudSettings(element);
110+
}
111+
}
112+
113+
private void monitorHudSettings(HudElement element) {
114+
for (SettingGroup group : element.settings) {
115+
for (Setting<?> setting : group) {
116+
wrapHudSettingCallback(element, setting);
117+
}
118+
}
119+
}
120+
81121
/**
82122
* Wrap a setting's onChanged callback to broadcast value changes
83123
*/
@@ -121,12 +161,47 @@ private <T> void wrapSettingCallback(Module module, Setting<T> setting) {
121161
}
122162
}
123163

164+
@SuppressWarnings("unchecked")
165+
private <T> void wrapHudSettingCallback(HudElement element, Setting<T> setting) {
166+
try {
167+
var onChangedField = Setting.class.getDeclaredField("onChanged");
168+
onChangedField.setAccessible(true);
169+
Consumer<T> originalCallback = (Consumer<T>) onChangedField.get(setting);
170+
171+
if (originalCallback != null) {
172+
originalCallbacks.put(setting, originalCallback);
173+
}
174+
175+
Consumer<T> wrappedCallback = value -> {
176+
if (originalCallback != null) {
177+
try {
178+
originalCallback.accept(value);
179+
} catch (Exception e) {
180+
LOG.error("Error in original HUD callback for {}.{}: {}",
181+
element.info != null ? element.info.name : element.getClass().getSimpleName(), setting.name, e.getMessage());
182+
}
183+
}
184+
185+
if (server.isRunning()) {
186+
server.broadcastHudSettingChange(element, setting);
187+
}
188+
};
189+
190+
onChangedField.set(setting, wrappedCallback);
191+
192+
} catch (Exception e) {
193+
LOG.error("Failed to wrap callback for HUD setting {}.{}: {}",
194+
element.info != null ? element.info.name : element.getClass().getSimpleName(), setting.name, e.getMessage());
195+
}
196+
}
197+
124198
/**
125199
* Stop monitoring (cleanup)
126200
*/
127201
public void stopMonitoring() {
128202
LOG.info("Stopping event monitoring");
129203
// Could restore original callbacks here if needed
130204
originalCallbacks.clear();
205+
hudStates.clear();
131206
}
132207
}

src/main/java/com/cope/meteorwebgui/gui/WebGUITab.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.cope.meteorwebgui.MeteorWebGUIAddon;
44
import com.cope.meteorwebgui.systems.WebGUIConfig;
5+
import com.cope.meteorwebgui.util.BrowserHelper;
56
import meteordevelopment.meteorclient.gui.GuiTheme;
67
import meteordevelopment.meteorclient.gui.tabs.Tab;
78
import meteordevelopment.meteorclient.gui.tabs.TabScreen;
@@ -62,9 +63,7 @@ public void initWidgets() {
6263

6364
// URL info
6465
String url = "http://" + WebGUIConfig.get().host.get() + ":" + WebGUIConfig.get().port.get();
65-
buttonRow.add(theme.button("Copy URL")).expandX().widget().action = () -> {
66-
mc.keyboard.setClipboard(url);
67-
};
66+
buttonRow.add(theme.button("Open in Browser")).expandX().widget().action = () -> BrowserHelper.openUrl(url);
6867

6968
} else {
7069
buttonRow.add(theme.button("Start Server")).expandX().widget().action = () -> {
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package com.cope.meteorwebgui.hud;
2+
3+
import com.cope.meteorwebgui.mapping.HudMapper;
4+
import com.google.gson.JsonArray;
5+
import meteordevelopment.meteorclient.systems.hud.HudElement;
6+
import meteordevelopment.meteorclient.systems.hud.HudElementInfo;
7+
import meteordevelopment.meteorclient.utils.render.color.Color;
8+
9+
import java.util.ArrayList;
10+
import java.util.Collection;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.concurrent.ConcurrentHashMap;
14+
import java.util.concurrent.atomic.AtomicBoolean;
15+
16+
/**
17+
* Lightweight capture helper that records text draw calls emitted by HUD elements.
18+
* Heavy processing happens elsewhere; this class stays on the render thread hot path only.
19+
*/
20+
public final class HudPreviewCapture {
21+
private static final ThreadLocal<CaptureBuffer> ACTIVE_BUFFER = new ThreadLocal<>();
22+
private static final Map<String, HudPreviewSnapshot> SNAPSHOTS = new ConcurrentHashMap<>();
23+
private static final AtomicBoolean ENABLED = new AtomicBoolean(false);
24+
25+
private HudPreviewCapture() {}
26+
27+
public static void setEnabled(boolean enabled) {
28+
ENABLED.set(enabled);
29+
if (!enabled) {
30+
ACTIVE_BUFFER.remove();
31+
SNAPSHOTS.clear();
32+
}
33+
}
34+
35+
public static boolean isEnabled() {
36+
return ENABLED.get();
37+
}
38+
39+
public static void begin(HudElement element) {
40+
if (!ENABLED.get() || element == null) return;
41+
ACTIVE_BUFFER.set(new CaptureBuffer(element));
42+
}
43+
44+
public static void end() {
45+
CaptureBuffer buffer = ACTIVE_BUFFER.get();
46+
ACTIVE_BUFFER.remove();
47+
if (!ENABLED.get() || buffer == null) return;
48+
49+
HudPreviewSnapshot snapshot = buffer.build();
50+
if (snapshot != null) {
51+
SNAPSHOTS.put(snapshot.getName(), snapshot);
52+
}
53+
}
54+
55+
public static void recordText(String text, double x, double y, Color color, boolean shadow, double scale) {
56+
CaptureBuffer buffer = ACTIVE_BUFFER.get();
57+
if (buffer == null) return;
58+
buffer.addLine(text, x, y, color, shadow, scale);
59+
}
60+
61+
public static void markNonText() {
62+
CaptureBuffer buffer = ACTIVE_BUFFER.get();
63+
if (buffer == null) return;
64+
buffer.markNonText();
65+
}
66+
67+
public static Collection<HudPreviewSnapshot> copySnapshots() {
68+
return List.copyOf(SNAPSHOTS.values());
69+
}
70+
71+
public static JsonArray serializeSnapshots() {
72+
JsonArray array = new JsonArray();
73+
for (HudPreviewSnapshot snapshot : SNAPSHOTS.values()) {
74+
array.add(snapshot.toJson());
75+
}
76+
return array;
77+
}
78+
79+
private static final class CaptureBuffer {
80+
private final HudElement element;
81+
private final List<HudTextLine> lines = new ArrayList<>();
82+
private boolean hasNonText;
83+
84+
private CaptureBuffer(HudElement element) {
85+
this.element = element;
86+
}
87+
88+
private void addLine(String text, double x, double y, Color color, boolean shadow, double scale) {
89+
if (text == null || text.isEmpty()) return;
90+
lines.add(new HudTextLine(text, x, y, color != null ? color.getPacked() : Color.WHITE.getPacked(), shadow, scale));
91+
}
92+
93+
private void markNonText() {
94+
hasNonText = true;
95+
}
96+
97+
private HudPreviewSnapshot build() {
98+
if (element == null) return null;
99+
HudElementInfo<?> info = element.info;
100+
String name = HudMapper.getElementIdentifier(element);
101+
String title = info != null ? info.title : name;
102+
String description = info != null ? info.description : "";
103+
String group = info != null && info.group != null ? info.group.title() : "HUD";
104+
105+
return new HudPreviewSnapshot(
106+
name,
107+
title,
108+
description,
109+
group,
110+
element.isActive(),
111+
element.getX(),
112+
element.getY(),
113+
element.getWidth(),
114+
element.getHeight(),
115+
hasNonText,
116+
lines,
117+
System.currentTimeMillis()
118+
);
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)