Skip to content

Commit edb0423

Browse files
committed
Cull sent-chunk displays with CraftEngine
1 parent a6d620b commit edb0423

15 files changed

Lines changed: 1125 additions & 17 deletions

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,10 @@ against Paper 26.2.
7070
overridden in `material.yml` under `CustomItems`. It also supplies display
7171
lighting: InteractionVisualizer shares its light-block reference counts so
7272
CraftEngine furniture and IV displays do not remove each other's light, and
73-
performs no lighting task while idle. CraftEngine is not bundled and the
74-
plugin behaves exactly as before when it is absent.
73+
performs no lighting task while idle. With `Settings.HideIfViewObstructed`,
74+
IV registers only its sent-chunk candidates in CraftEngine's entity-culling
75+
API for a second-stage ray-traced wall-occlusion check. CraftEngine is not
76+
bundled and the plugin behaves exactly as before when it is absent.
7577
- [OpenInv](https://dev.bukkit.org/projects/openinv)
7678
- [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.6245/)
7779
- Essentials, SuperVanish, PremiumVanish, and CMI

build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,21 +205,29 @@ val verifyCustomContentIsolation = tasks.register("verifyCustomContentIsolation"
205205
val allowedCraftEngineLightSource = file(
206206
"common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineLightManager.java",
207207
).canonicalFile
208+
val allowedCraftEngineCullingSource = file(
209+
"common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineViewerCullingManager.java",
210+
).canonicalFile
208211
val managerSource = file(
209212
"common/src/main/java/com/loohp/interactionvisualizer/integration/CustomContentManager.java",
210213
).canonicalFile
211214
val lightLoaderSource = file(
212215
"common/src/main/java/com/loohp/interactionvisualizer/managers/LightManager.java",
213216
).canonicalFile
217+
val cullingLoaderSource = file(
218+
"common/src/main/java/com/loohp/interactionvisualizer/integration/ViewerCullingManagerLoader.java",
219+
).canonicalFile
214220
val stableApiClass = "net.momirealms.craftengine.bukkit.api.CraftEngineItems"
215221
val lightSentinelClass = "net.momirealms.craftengine.bukkit.api.BukkitAdaptor"
222+
val cullingSentinelClass = "net.momirealms.craftengine.core.entity.culling.Cullable"
216223
val craftEngineToken = Regex("net\\.momirealms\\.craftengine(?:\\.[A-Za-z_$][A-Za-z0-9_$]*)+")
217224
val violations = sources.files.flatMap { source ->
218225
craftEngineToken.findAll(source.readText()).mapNotNull { match ->
219226
val allowed = when (source.canonicalFile) {
220227
allowedCraftEngineSource, managerSource -> match.value == stableApiClass
221228
lightLoaderSource -> match.value == lightSentinelClass
222-
allowedCraftEngineLightSource -> true
229+
cullingLoaderSource -> match.value == cullingSentinelClass
230+
allowedCraftEngineLightSource, allowedCraftEngineCullingSource -> true
223231
else -> false
224232
}
225233
if (allowed) null else "${source.relativeTo(rootDir)}: ${match.value}"

common/src/main/java/com/loohp/interactionvisualizer/InteractionVisualizer.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import com.loohp.interactionvisualizer.debug.PerformanceBlockScene;
2727
import com.loohp.interactionvisualizer.debug.PerformanceScene;
2828
import com.loohp.interactionvisualizer.integration.CustomContentManager;
29+
import com.loohp.interactionvisualizer.integration.ViewerCullingManager;
30+
import com.loohp.interactionvisualizer.integration.ViewerCullingManagerLoader;
2931
import com.loohp.interactionvisualizer.managers.AsyncExecutorManager;
3032
import com.loohp.interactionvisualizer.managers.LangManager;
3133
import com.loohp.interactionvisualizer.managers.LightManager;
@@ -126,8 +128,11 @@ public class InteractionVisualizer extends JavaPlugin {
126128
public static int blockUpdateMaxDirtyPerTick = 64;
127129

128130
private boolean blockUpdateModeInitialized;
131+
private boolean cullingLifecycleInitialized;
132+
private boolean cullingProviderWarningLogged;
129133

130134
public static ILightManager lightManager;
135+
public static ViewerCullingManager viewerCullingManager = ViewerCullingManager.DISABLED;
131136
public static PreferenceManager preferenceManager;
132137
public static AsyncExecutorManager asyncExecutorManager;
133138

@@ -194,6 +199,10 @@ public void onEnable() {
194199
craftEngineHooked = true;
195200
}
196201
}
202+
if (configureViewerCulling()) {
203+
craftEngineHooked = true;
204+
}
205+
cullingLifecycleInitialized = true;
197206
if (craftEngineHooked) {
198207
hookMessage("CraftEngine");
199208
}
@@ -262,6 +271,8 @@ public void onDisable() {
262271
lightManager.shutdown();
263272
lightManager = ILightManager.DUMMY_INSTANCE;
264273
}
274+
viewerCullingManager.shutdown();
275+
viewerCullingManager = ViewerCullingManager.DISABLED;
265276
CustomContentManager.shutdown();
266277
if (preferenceManager != null) {
267278
preferenceManager.close();
@@ -335,6 +346,9 @@ public void loadConfig() {
335346

336347
disabledWorlds = new HashSet<>(getConfiguration().getStringList("Settings.DisabledWorlds"));
337348
hideIfObstructed = getConfiguration().getBoolean("Settings.HideIfViewObstructed");
349+
if (cullingLifecycleInitialized) {
350+
configureViewerCulling();
351+
}
338352

339353
lightUpdatePeriod = getConfiguration().getInt("LightUpdate.Period");
340354
if (lightManager != null) {
@@ -376,4 +390,46 @@ public void loadConfig() {
376390
getServer().getPluginManager().callEvent(new InteractionVisualizerReloadEvent());
377391
}
378392

393+
private boolean configureViewerCulling() {
394+
if (!hideIfObstructed) {
395+
boolean backendChanged = viewerCullingManager.enabled();
396+
viewerCullingManager.shutdown();
397+
viewerCullingManager = ViewerCullingManager.DISABLED;
398+
if (backendChanged && cullingLifecycleInitialized) {
399+
DisplayManager.onCullingBackendChanged();
400+
}
401+
return false;
402+
}
403+
if (viewerCullingManager.enabled()) {
404+
return true;
405+
}
406+
viewerCullingManager.shutdown();
407+
viewerCullingManager = ViewerCullingManager.DISABLED;
408+
if (!isPluginEnabled("CraftEngine")) {
409+
if (!cullingProviderWarningLogged) {
410+
cullingProviderWarningLogged = true;
411+
getLogger().warning("Settings.HideIfViewObstructed requires CraftEngine 26.7+; "
412+
+ "using sent-chunk visibility without occlusion culling.");
413+
}
414+
return false;
415+
}
416+
ViewerCullingManager manager = ViewerCullingManagerLoader.createCraftEngine(
417+
this, DisplayManager::onCullingVisibility).orElse(ViewerCullingManager.DISABLED);
418+
if (!manager.enabled()) {
419+
manager.shutdown();
420+
if (!cullingProviderWarningLogged) {
421+
cullingProviderWarningLogged = true;
422+
getLogger().warning("CraftEngine entity culling and ray tracing must both be enabled "
423+
+ "for Settings.HideIfViewObstructed; using sent-chunk visibility only.");
424+
}
425+
return false;
426+
}
427+
cullingProviderWarningLogged = false;
428+
viewerCullingManager = manager;
429+
if (cullingLifecycleInitialized) {
430+
DisplayManager.onCullingBackendChanged();
431+
}
432+
return true;
433+
}
434+
379435
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.integration;
13+
14+
/** Immutable provider-neutral bounds for one logical display. */
15+
public record CullingBounds(
16+
double minX, double minY, double minZ,
17+
double maxX, double maxY, double maxZ,
18+
int maxDistance, double expansion, boolean rayTracing) {
19+
20+
public CullingBounds {
21+
if (!Double.isFinite(minX) || !Double.isFinite(minY) || !Double.isFinite(minZ)
22+
|| !Double.isFinite(maxX) || !Double.isFinite(maxY) || !Double.isFinite(maxZ)
23+
|| !Double.isFinite(expansion)) {
24+
throw new IllegalArgumentException("Culling bounds must be finite");
25+
}
26+
if (maxX < minX || maxY < minY || maxZ < minZ) {
27+
throw new IllegalArgumentException("Culling bounds must be ordered");
28+
}
29+
if (maxDistance < 0 || expansion < 0.0D) {
30+
throw new IllegalArgumentException("Culling distance and expansion cannot be negative");
31+
}
32+
}
33+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.integration;
13+
14+
import org.bukkit.entity.Player;
15+
16+
import java.util.UUID;
17+
import java.util.Set;
18+
19+
/** Optional second-stage occlusion culling for sent-chunk display candidates. */
20+
public interface ViewerCullingManager {
21+
22+
ViewerCullingManager DISABLED = new ViewerCullingManager() {
23+
@Override
24+
public boolean enabled() {
25+
return false;
26+
}
27+
28+
@Override
29+
public boolean track(Player viewer, UUID logicalId, CullingBounds bounds) {
30+
return false;
31+
}
32+
33+
@Override
34+
public void update(UUID logicalId, CullingBounds bounds) {
35+
}
36+
37+
@Override
38+
public void untrack(UUID viewerId, UUID logicalId) {
39+
}
40+
41+
@Override
42+
public void clearViewer(UUID viewerId) {
43+
}
44+
45+
@Override
46+
public void clearLogical(UUID logicalId) {
47+
}
48+
49+
@Override
50+
public void retainLogical(UUID logicalId, Set<UUID> viewerIds) {
51+
}
52+
53+
@Override
54+
public void shutdown() {
55+
}
56+
57+
@Override
58+
public int retainedRegistrations() {
59+
return 0;
60+
}
61+
};
62+
63+
boolean enabled();
64+
65+
/** Returns true when this backend owns visibility for the candidate. */
66+
boolean track(Player viewer, UUID logicalId, CullingBounds bounds);
67+
68+
void update(UUID logicalId, CullingBounds bounds);
69+
70+
void untrack(UUID viewerId, UUID logicalId);
71+
72+
void clearViewer(UUID viewerId);
73+
74+
void clearLogical(UUID logicalId);
75+
76+
/** Removes registrations for this logical that are no longer local candidates. */
77+
void retainLogical(UUID logicalId, Set<UUID> viewerIds);
78+
79+
void shutdown();
80+
81+
int retainedRegistrations();
82+
83+
@FunctionalInterface
84+
interface VisibilityListener {
85+
86+
void visibilityChanged(UUID viewerId, UUID logicalId, boolean visible);
87+
}
88+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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.integration;
13+
14+
import org.bukkit.plugin.Plugin;
15+
16+
import java.lang.reflect.InvocationTargetException;
17+
import java.util.Optional;
18+
import java.util.logging.Level;
19+
20+
/** Reflection loader for the optional CraftEngine culling implementation. */
21+
public final class ViewerCullingManagerLoader {
22+
23+
static final String CRAFT_ENGINE_SENTINEL =
24+
"net.momirealms.craftengine.core.entity.culling.Cullable";
25+
static final String CRAFT_ENGINE_IMPLEMENTATION =
26+
"com.loohp.interactionvisualizer.integration.craftengine.CraftEngineViewerCullingManager";
27+
28+
private ViewerCullingManagerLoader() {
29+
}
30+
31+
public static Optional<ViewerCullingManager> createCraftEngine(
32+
Plugin plugin, ViewerCullingManager.VisibilityListener listener) {
33+
try {
34+
return Optional.of(load(plugin.getClass().getClassLoader(), CRAFT_ENGINE_SENTINEL,
35+
CRAFT_ENGINE_IMPLEMENTATION, plugin, listener));
36+
} catch (ReflectiveOperationException | LinkageError | ClassCastException exception) {
37+
Throwable cause = exception instanceof InvocationTargetException invocation
38+
&& invocation.getCause() != null ? invocation.getCause() : exception;
39+
plugin.getLogger().log(Level.WARNING,
40+
"Could not enable CraftEngine display culling; using sent-chunk visibility only", cause);
41+
return Optional.empty();
42+
}
43+
}
44+
45+
static ViewerCullingManager load(ClassLoader classLoader, String sentinelClass,
46+
String implementationClass, Plugin plugin,
47+
ViewerCullingManager.VisibilityListener listener)
48+
throws ReflectiveOperationException {
49+
Class.forName(sentinelClass, false, classLoader);
50+
return Class.forName(implementationClass, true, classLoader)
51+
.asSubclass(ViewerCullingManager.class)
52+
.getDeclaredConstructor(Plugin.class, ViewerCullingManager.VisibilityListener.class)
53+
.newInstance(plugin, listener);
54+
}
55+
}

0 commit comments

Comments
 (0)