Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,33 @@ The production plugin JAR is written to `build/libs/InteractionVisualizer-<versi
`check` includes unit tests, the Paper 26.1.2 compilation, and a second compile
against Paper 26.2.

## Performance rollout and diagnostics

New installations enable the bounded visibility restore, dropped-label spatial
culling, static packet items, static animation anchors, and coordinated block
updates. Existing explicit `false` values remain unchanged and produce one
migration reminder. Every path has its own rollback switch in `config.yml`;
event-driven block updates require a restart, while the other switches reload.

Use `/iv perf start <label>` and `/iv perf stop` around a stable sample window.
The resulting `IV_PERF` JSON includes viewer candidates/full reconciles,
dropped-item spatial and full-scan candidates, block queues, preference I/O and
SQL operations, packet operations, and anchor entity operations. Every plugin
disable also emits `IV_PERF_SHUTDOWN`; `totalRetained` should be zero in repeated
enable/disable leak tests.

## Optional integrations

- [CraftEngine](https://github.com/Xiao-MoMi/craft-engine) 26.7.2: optional
custom-item ID recognition. CraftEngine items can be selected by the third
field of an item-label blacklist rule, and their display pose can be
overridden in `material.yml` under `CustomItems`. CraftEngine is not bundled
and the plugin behaves exactly as before when it is absent.
- [LightAPI](https://www.spigotmc.org/resources/lightapi-fork.48247/)
overridden in `material.yml` under `CustomItems`. It also supplies display
lighting: InteractionVisualizer shares its light-block reference counts so
CraftEngine furniture and IV displays do not remove each other's light, and
performs no lighting task while idle. With `Settings.HideIfViewObstructed`,
IV registers only its sent-chunk candidates in CraftEngine's entity-culling
API for a second-stage ray-traced wall-occlusion check. CraftEngine is not
bundled and the plugin behaves exactly as before when it is absent.
- [OpenInv](https://dev.bukkit.org/projects/openinv)
- [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.6245/)
- Essentials, SuperVanish, PremiumVanish, and CMI
Expand Down
19 changes: 17 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ dependencies {
compileOnly("me.clip:placeholderapi:2.11.7")
compileOnly("net.momirealms:craft-engine-core:$craftEngineVersion")
compileOnly("net.momirealms:craft-engine-bukkit:$craftEngineVersion")
compileOnly(files("common/lib/LightAPI-fork-3.5.2.jar"))

implementation("net.momirealms:sparrow-yaml:1.0.7")
implementation("net.momirealms:sparrow-heart:$sparrowHeartVersion")
Expand All @@ -49,7 +48,6 @@ dependencies {
paper26_2CompileClasspath("me.clip:placeholderapi:2.11.7")
paper26_2CompileClasspath("net.momirealms:craft-engine-core:$craftEngineVersion")
paper26_2CompileClasspath("net.momirealms:craft-engine-bukkit:$craftEngineVersion")
paper26_2CompileClasspath(files("common/lib/LightAPI-fork-3.5.2.jar"))
}

java {
Expand Down Expand Up @@ -224,15 +222,32 @@ val verifyCustomContentIsolation = tasks.register("verifyCustomContentIsolation"
val allowedCraftEngineSource = file(
"common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineCustomContentBridge.java",
).canonicalFile
val allowedCraftEngineLightSource = file(
"common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineLightManager.java",
).canonicalFile
val allowedCraftEngineCullingSource = file(
"common/src/main/java/com/loohp/interactionvisualizer/integration/craftengine/CraftEngineViewerCullingManager.java",
).canonicalFile
val managerSource = file(
"common/src/main/java/com/loohp/interactionvisualizer/integration/CustomContentManager.java",
).canonicalFile
val lightLoaderSource = file(
"common/src/main/java/com/loohp/interactionvisualizer/managers/LightManager.java",
).canonicalFile
val cullingLoaderSource = file(
"common/src/main/java/com/loohp/interactionvisualizer/integration/ViewerCullingManagerLoader.java",
).canonicalFile
val stableApiClass = "net.momirealms.craftengine.bukkit.api.CraftEngineItems"
val lightSentinelClass = "net.momirealms.craftengine.bukkit.api.BukkitAdaptor"
val cullingSentinelClass = "net.momirealms.craftengine.core.entity.culling.Cullable"
val craftEngineToken = Regex("net\\.momirealms\\.craftengine(?:\\.[A-Za-z_$][A-Za-z0-9_$]*)+")
val violations = sources.files.flatMap { source ->
craftEngineToken.findAll(source.readText()).mapNotNull { match ->
val allowed = when (source.canonicalFile) {
allowedCraftEngineSource, managerSource -> match.value == stableApiClass
lightLoaderSource -> match.value == lightSentinelClass
cullingLoaderSource -> match.value == cullingSentinelClass
allowedCraftEngineLightSource, allowedCraftEngineCullingSource -> true
else -> false
}
if (allowed) null else "${source.relativeTo(rootDir)}: ${match.value}"
Expand Down
Binary file removed common/lib/LightAPI-fork-3.5.2.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import com.loohp.interactionvisualizer.debug.PerformanceBlockScene;
import com.loohp.interactionvisualizer.debug.PerformanceScene;
import com.loohp.interactionvisualizer.integration.CustomContentManager;
import com.loohp.interactionvisualizer.integration.ViewerCullingManager;
import com.loohp.interactionvisualizer.integration.ViewerCullingManagerLoader;
import com.loohp.interactionvisualizer.managers.AsyncExecutorManager;
import com.loohp.interactionvisualizer.managers.LangManager;
import com.loohp.interactionvisualizer.managers.LightManager;
Expand Down Expand Up @@ -75,10 +77,15 @@ public class InteractionVisualizer extends JavaPlugin {
public static final int BSTATS_PLUGIN_ID = 7024;
public static final String CONFIG_ID = "config";
public static final Set<String> SUPPORTED_MINECRAFT_VERSIONS = Set.of("26.1.2", "26.2");
private static final String PERFORMANCE_MIGRATION_MARKER =
".paper26-performance-defaults-advised";

public static InteractionVisualizer plugin = null;

/** @deprecated LightAPI is no longer used; retained for binary compatibility. */
@Deprecated(forRemoval = false)
public static Boolean lightapi = false;
public static Boolean craftEngineLight = false;
public static Boolean openinv = false;

public static Set<String> exemptBlocks = new HashSet<>();
Expand Down Expand Up @@ -109,20 +116,25 @@ public class InteractionVisualizer extends JavaPlugin {

public static boolean defaultDisabledAll = false;
/** A/B switch: virtual item remains authoritative while an invisible tracker stays stationary. */
public static boolean staticVirtualItemAnchorsDuringAnimation = false;
public static boolean staticVirtualItemAnchorsDuringAnimation = true;
/** A/B switch: eligible stationary virtual items are tracked and rendered entirely by packets. */
public static boolean packetOnlyStaticVirtualItems = false;
public static boolean packetOnlyStaticVirtualItems = true;
/** A/B switch: safe animated virtual items use Typewriter-style per-viewer packet tracking. */
public static boolean packetOnlyAnimatedVirtualItems = false;
/** A/B switch: smooths visibility recovery bursts; hides are always immediate. */
public static boolean visibilityRateLimiting = false;
public static boolean visibilityRateLimiting = true;
public static int visibilityRateLimitBucketSize = 128;
public static int visibilityRateLimitRestorePerTick = 32;
/** A/B switch: coalesces block changes and updates tracked blocks from fixed-budget loops. */
public static boolean eventDrivenBlockUpdates = false;
public static boolean eventDrivenBlockUpdates = true;
public static int blockUpdateMaxDirtyPerTick = 64;

private boolean blockUpdateModeInitialized;
private boolean cullingLifecycleInitialized;
private boolean cullingProviderWarningLogged;

public static ILightManager lightManager;
public static ViewerCullingManager viewerCullingManager = ViewerCullingManager.DISABLED;
public static PreferenceManager preferenceManager;
public static AsyncExecutorManager asyncExecutorManager;

Expand Down Expand Up @@ -160,18 +172,9 @@ public void onEnable() {
new ThreadPoolExecutor.CallerRunsPolicy());
asyncExecutorManager = new AsyncExecutorManager(threadPool);

if (isPluginEnabled("LightAPI")) {
try {
Class.forName("ru.beykerykt.lightapi.utils.Debug");
hookMessage("LightAPI");
lightapi = true;
lightManager = new LightManager(this);
} catch (ClassNotFoundException ignored) {
}
}
if (!lightapi) {
lightManager = ILightManager.DUMMY_INSTANCE;
}
lightapi = false;
craftEngineLight = false;
lightManager = ILightManager.DUMMY_INSTANCE;
if (isPluginEnabled("OpenInv")) {
hookMessage("OpenInv");
openinv = true;
Expand All @@ -180,16 +183,32 @@ public void onEnable() {
if (!getDataFolder().exists()) {
getDataFolder().mkdirs();
}
File configFile = new File(getDataFolder(), "config.yml");
boolean existingConfiguration = configFile.isFile();
try {
Config.loadConfig(CONFIG_ID, new File(getDataFolder(), "config.yml"), getClass().getClassLoader().getResourceAsStream("config.yml"), getClass().getClassLoader().getResourceAsStream("config.yml"), true);
Config.loadConfig(CONFIG_ID, configFile, getClass().getClassLoader().getResourceAsStream("config.yml"), getClass().getClassLoader().getResourceAsStream("config.yml"), true);
} catch (IOException e) {
e.printStackTrace();
getServer().getPluginManager().disablePlugin(this);
return;
}
loadConfig();

if (CustomContentManager.initialize(this).contains("craftengine")) {
advisePerformanceMigrationOnce(existingConfiguration);

boolean craftEngineHooked = CustomContentManager.initialize(this).contains("craftengine");
if (isPluginEnabled("CraftEngine")) {
ILightManager craftEngineManager = LightManager.createCraftEngine(this, lightUpdatePeriod).orElse(null);
if (craftEngineManager != null) {
lightManager = craftEngineManager;
craftEngineLight = true;
craftEngineHooked = true;
}
}
if (configureViewerCulling()) {
craftEngineHooked = true;
}
cullingLifecycleInitialized = true;
if (craftEngineHooked) {
hookMessage("CraftEngine");
}

Expand Down Expand Up @@ -253,18 +272,41 @@ public void onEnable() {
@Override
public void onDisable() {
shutdownPerformanceScenes();
int retainedLightState = 0;
ILightManager disablingLightManager = lightManager;
if (disablingLightManager != null) {
disablingLightManager.shutdown();
retainedLightState = disablingLightManager.retainedStateCount();
lightManager = ILightManager.DUMMY_INSTANCE;
}
ViewerCullingManager disablingCullingManager = viewerCullingManager;
disablingCullingManager.shutdown();
int retainedCullingRegistrations = disablingCullingManager.retainedRegistrations();
viewerCullingManager = ViewerCullingManager.DISABLED;
CustomContentManager.shutdown();
if (preferenceManager != null) {
preferenceManager.close();
int retainedPreferenceState = 0;
PreferenceManager disablingPreferenceManager = preferenceManager;
if (disablingPreferenceManager != null) {
disablingPreferenceManager.close();
retainedPreferenceState = disablingPreferenceManager.retainedStateCount();
preferenceManager = null;
}
Database.close();
TaskManager.shutdown();
DisplayManager.shutdown();
TileEntityManager.shutdown();
PlayerLocationManager.clearCache();
playerTrackingRange.clear();
LegacyTextComponentCache.invalidateAll();
if (asyncExecutorManager != null) {
asyncExecutorManager.close();
int retainedAsyncTasks = 0;
AsyncExecutorManager disablingAsyncExecutorManager = asyncExecutorManager;
if (disablingAsyncExecutorManager != null) {
disablingAsyncExecutorManager.close();
retainedAsyncTasks = disablingAsyncExecutorManager.retainedTaskCount();
asyncExecutorManager = null;
}
PerformanceMetrics.logShutdownState(this, retainedAsyncTasks,
retainedPreferenceState, retainedLightState, retainedCullingRegistrations);
getServer().getConsoleSender().sendMessage(Component.text(
"[InteractionVisualizer] Disabled; all display entities removed.", NamedTextColor.RED));
}
Expand Down Expand Up @@ -300,6 +342,43 @@ public SparrowConfiguration getConfiguration() {
return Config.getConfig(CONFIG_ID).getConfiguration();
}

private void advisePerformanceMigrationOnce(boolean existingConfiguration) {
File marker = new File(getDataFolder(), PERFORMANCE_MIGRATION_MARKER);
if (marker.isFile()) {
return;
}
try {
if (!marker.createNewFile()) {
return;
}
} catch (IOException exception) {
getLogger().log(Level.WARNING,
"Unable to persist the Paper 26 performance migration notice marker", exception);
}
if (existingConfiguration && hasPreservedPerformanceOptOut()) {
getLogger().warning("Paper 26 performance defaults are enabled for new installations. "
+ "Your explicit false values were preserved; review StaticAnchorDuringAnimation, "
+ "PacketOnlyStatic, VisibilityRateLimit, BlockUpdates.EventDriven, and dropped-item "
+ "VisibilityCulling/VisibilityRateLimit when you are ready to migrate.");
}
}

private boolean hasPreservedPerformanceOptOut() {
SparrowConfiguration configuration = getConfiguration();
return !configuration.getBoolean(
"Settings.Performance.VirtualItems.StaticAnchorDuringAnimation")
|| !configuration.getBoolean(
"Settings.Performance.VirtualItems.PacketOnlyStatic")
|| !configuration.getBoolean(
"Settings.Performance.VisibilityRateLimit.Enabled")
|| !configuration.getBoolean(
"Settings.Performance.BlockUpdates.EventDriven")
|| !configuration.getBoolean(
"Entities.Item.Options.VisibilityCulling.Enabled")
|| !configuration.getBoolean(
"Entities.Item.Options.VisibilityRateLimit.Enabled");
}

public void loadConfig() {
Config config = Config.getConfig(CONFIG_ID);
config.reload();
Expand All @@ -326,8 +405,14 @@ public void loadConfig() {

disabledWorlds = new HashSet<>(getConfiguration().getStringList("Settings.DisabledWorlds"));
hideIfObstructed = getConfiguration().getBoolean("Settings.HideIfViewObstructed");
if (cullingLifecycleInitialized) {
configureViewerCulling();
}

lightUpdatePeriod = getConfiguration().getInt("LightUpdate.Period");
if (lightManager != null) {
lightManager.setUpdatePeriod(lightUpdatePeriod);
}

updaterEnabled = getConfiguration().getBoolean("Options.Updater");

Expand All @@ -341,6 +426,8 @@ public void loadConfig() {
"Settings.Performance.VirtualItems.StaticAnchorDuringAnimation");
packetOnlyStaticVirtualItems = getConfiguration().getBoolean(
"Settings.Performance.VirtualItems.PacketOnlyStatic");
packetOnlyAnimatedVirtualItems = getConfiguration().getBoolean(
"Settings.Performance.VirtualItems.PacketOnlyAnimated");
visibilityRateLimiting = getConfiguration().getBoolean(
"Settings.Performance.VisibilityRateLimit.Enabled");
visibilityRateLimitBucketSize = Math.max(1, getConfiguration().getInt(
Expand All @@ -362,4 +449,46 @@ public void loadConfig() {
getServer().getPluginManager().callEvent(new InteractionVisualizerReloadEvent());
}

private boolean configureViewerCulling() {
if (!hideIfObstructed) {
boolean backendChanged = viewerCullingManager.enabled();
viewerCullingManager.shutdown();
viewerCullingManager = ViewerCullingManager.DISABLED;
if (backendChanged && cullingLifecycleInitialized) {
DisplayManager.onCullingBackendChanged();
}
return false;
}
if (viewerCullingManager.enabled()) {
return true;
}
viewerCullingManager.shutdown();
viewerCullingManager = ViewerCullingManager.DISABLED;
if (!isPluginEnabled("CraftEngine")) {
if (!cullingProviderWarningLogged) {
cullingProviderWarningLogged = true;
getLogger().warning("Settings.HideIfViewObstructed requires CraftEngine 26.7+; "
+ "using sent-chunk visibility without occlusion culling.");
}
return false;
}
ViewerCullingManager manager = ViewerCullingManagerLoader.createCraftEngine(
this, DisplayManager::onCullingVisibility).orElse(ViewerCullingManager.DISABLED);
if (!manager.enabled()) {
manager.shutdown();
if (!cullingProviderWarningLogged) {
cullingProviderWarningLogged = true;
getLogger().warning("CraftEngine entity culling and ray tracing must both be enabled "
+ "for Settings.HideIfViewObstructed; using sent-chunk visibility only.");
}
return false;
}
cullingProviderWarningLogged = false;
viewerCullingManager = manager;
if (cullingLifecycleInitialized) {
DisplayManager.onCullingBackendChanged();
}
return true;
}

}
Loading
Loading