411 lines
19 KiB
Diff
411 lines
19 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||
From: Luminol Contributors <luminol@example.com>
|
||
Date: Tue, 22 Jun 2077 00:00:00 +0800
|
||
Subject: [PATCH] LagFixer: VehicleMotion reducer and ExplosionOptimizer
|
||
|
||
Ports two LagFixer modules as built-in server patches:
|
||
|
||
VehicleMotionReducer
|
||
- Replaces newly-placed boats/minecarts with silent optimized variants
|
||
that suppress ambient sounds and reduce idle-tick cost.
|
||
- Optionally removes chest-minecarts spawned by mineshafts.
|
||
- Config: optimizations.vehicle-motion
|
||
|
||
ExplosionOptimizer
|
||
- Clamps explosion yield (radius) per entity type.
|
||
- Prevents chain explosions within a configurable radius/cooldown.
|
||
- Optionally cancels TNT / creeper / crystal explosions entirely while
|
||
still applying configurable damage + knockback + sound effects.
|
||
- Config: optimizations.explosion-optimizer
|
||
|
||
Both are fully Folia-safe: event handlers fire on the owning region
|
||
thread, and no cross-region state is shared.
|
||
|
||
Co-authored-by: lajczik (https://github.com/lajczik/lagfixer)
|
||
Licensed under GPL-3.0
|
||
|
||
diff --git a/me/earthme/luminol/config/modules/optimizations/VehicleMotionConfig.java b/me/earthme/luminol/config/modules/optimizations/VehicleMotionConfig.java
|
||
new file mode 100644
|
||
index 0000000000000000000000000000000000000000..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||
--- /dev/null
|
||
+++ b/me/earthme/luminol/config/modules/optimizations/VehicleMotionConfig.java
|
||
@@ -0,0 +1,33 @@
|
||
+package me.earthme.luminol.config.modules.optimizations;
|
||
+
|
||
+import me.earthme.luminol.config.ILuminolConfig;
|
||
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||
+import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||
+
|
||
+@ConfigSerializable
|
||
+public class VehicleMotionConfig implements ILuminolConfig {
|
||
+
|
||
+ @Comment("Enable vehicle motion optimization.")
|
||
+ public boolean enabled = false;
|
||
+
|
||
+ @Comment("Optimize boats: replace with silent, low-overhead variant.")
|
||
+ public boolean boats = true;
|
||
+
|
||
+ @Comment("Optimize minecarts: replace with silent, low-overhead variant.\n"
|
||
+ + "Also removes chest-minecarts generated by mineshafts.")
|
||
+ public boolean minecarts = true;
|
||
+
|
||
+ @Comment("Remove minecart-with-chest entities spawned by structure generation\n"
|
||
+ + "(mineshaft loot minecarts). These accumulate over time and are rarely visited.")
|
||
+ public boolean removeMineshaftChestCarts = true;
|
||
+
|
||
+ @Override
|
||
+ public String getConfigurationPath() {
|
||
+ return "optimizations.vehicle-motion";
|
||
+ }
|
||
+}
|
||
diff --git a/me/earthme/luminol/config/modules/optimizations/ExplosionOptimizerConfig.java b/me/earthme/luminol/config/modules/optimizations/ExplosionOptimizerConfig.java
|
||
new file mode 100644
|
||
index 0000000000000000000000000000000000000000..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||
--- /dev/null
|
||
+++ b/me/earthme/luminol/config/modules/optimizations/ExplosionOptimizerConfig.java
|
||
@@ -0,0 +1,80 @@
|
||
+package me.earthme.luminol.config.modules.optimizations;
|
||
+
|
||
+import me.earthme.luminol.config.ILuminolConfig;
|
||
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
|
||
+import org.spongepowered.configurate.objectmapping.meta.Comment;
|
||
+
|
||
+@ConfigSerializable
|
||
+public class ExplosionOptimizerConfig implements ILuminolConfig {
|
||
+
|
||
+ @Comment("Enable explosion optimizer.")
|
||
+ public boolean enabled = false;
|
||
+
|
||
+ // ── Yield limit ──────────────────────────────────────────────────────────
|
||
+ @Comment("Cap explosion radius to limit block damage on large explosions.")
|
||
+ public boolean yieldLimitEnabled = true;
|
||
+
|
||
+ @Comment("Default maximum explosion radius (vanilla TNT = 4.0).")
|
||
+ public float yieldLimitDefault = 4.0f;
|
||
+
|
||
+ @Comment("Per-entity-type yield overrides (Bukkit EntityType name → max radius).\n"
|
||
+ + "Example: {END_CRYSTAL: 6.0, CREEPER: 3.0}")
|
||
+ public java.util.Map<String, Float> yieldLimitPerType = java.util.Map.of(
|
||
+ "END_CRYSTAL", 6.0f,
|
||
+ "WITHER_SKULL", 1.0f
|
||
+ );
|
||
+
|
||
+ // ── Anti-chain ───────────────────────────────────────────────────────────
|
||
+ @Comment("Prevent chain explosions within chain-radius of a recent explosion.")
|
||
+ public boolean antiChainEnabled = true;
|
||
+
|
||
+ @Comment("Prevent TNT→TNT chain reactions.")
|
||
+ public boolean preventTntChains = true;
|
||
+
|
||
+ @Comment("Prevent creeper→creeper chain triggers.")
|
||
+ public boolean preventCreeperChains = false;
|
||
+
|
||
+ @Comment("Prevent end-crystal chain detonations.")
|
||
+ public boolean preventCrystalChains = true;
|
||
+
|
||
+ @Comment("Remove TNT blocks exposed by an explosion (stops cascades).")
|
||
+ public boolean preventBlockIgnition = true;
|
||
+
|
||
+ @Comment("Radius (blocks) within which a second explosion is suppressed.")
|
||
+ public double chainRadius = 8.0;
|
||
+
|
||
+ @Comment("Milliseconds before the anti-chain protection expires.")
|
||
+ public long chainCooldownMs = 1000L;
|
||
+
|
||
+ // ── Management (cancel & simulate) ──────────────────────────────────────
|
||
+ @Comment("Replace certain explosions with simulated damage/knockback only\n"
|
||
+ + "(no block destruction).")
|
||
+ public boolean managementEnabled = false;
|
||
+
|
||
+ public boolean cancelTnt = true;
|
||
+ public boolean cancelCreepers = false;
|
||
+ public boolean cancelCrystals = true;
|
||
+ public boolean cancelFireballs = true;
|
||
+ public boolean cancelWitherSkulls = true;
|
||
+ public boolean cancelBlockExplosions = true;
|
||
+
|
||
+ @Comment("Apply simulated damage when explosion is cancelled.")
|
||
+ public boolean simulateDamage = false;
|
||
+ public float simulateDamageMultiplier = 2.5f;
|
||
+
|
||
+ @Comment("Play explosion sound when explosion is cancelled.")
|
||
+ public boolean simulateSound = true;
|
||
+ public float simulateSoundVolume = 1.0f;
|
||
+
|
||
+ @Comment("Apply knockback when explosion is cancelled.")
|
||
+ public boolean simulateKnockback = false;
|
||
+ public float simulateKnockbackMultiplier = 0.6f;
|
||
+
|
||
+ @Comment("Use Newton-Raphson fast inverse-square-root for knockback calculation\n"
|
||
+ + "(tiny accuracy trade-off, measurable speed gain on mass events).")
|
||
+ public boolean fastInvSqrt = true;
|
||
+
|
||
+ @Override
|
||
+ public String getConfigurationPath() {
|
||
+ return "optimizations.explosion-optimizer";
|
||
+ }
|
||
+}
|
||
diff --git a/me/earthme/luminol/vehicle/LuminolVehicleOptimizer.java b/me/earthme/luminol/vehicle/LuminolVehicleOptimizer.java
|
||
new file mode 100644
|
||
index 0000000000000000000000000000000000000000..cccccccccccccccccccccccccccccccccccccccc
|
||
--- /dev/null
|
||
+++ b/me/earthme/luminol/vehicle/LuminolVehicleOptimizer.java
|
||
@@ -0,0 +1,94 @@
|
||
+package me.earthme.luminol.vehicle;
|
||
+
|
||
+import me.earthme.luminol.config.modules.optimizations.VehicleMotionConfig;
|
||
+import net.minecraft.world.entity.vehicle.*;
|
||
+import net.minecraft.world.item.ItemStack;
|
||
+import net.minecraft.world.level.Level;
|
||
+import org.bukkit.Location;
|
||
+
|
||
+/**
|
||
+ * Replaces newly-placed vehicle entities with lightweight silent variants.
|
||
+ * The replacement happens synchronously on the region thread that placed the vehicle.
|
||
+ * Ported from LagFixer VehicleMotionReducer / VehicleWrapper (GPL-3.0).
|
||
+ */
|
||
+public final class LuminolVehicleOptimizer {
|
||
+
|
||
+ private LuminolVehicleOptimizer() {}
|
||
+
|
||
+ /**
|
||
+ * Called when a vehicle entity is added to the world.
|
||
+ * Returns true if the original entity was replaced (caller should remove original).
|
||
+ */
|
||
+ public static boolean tryOptimize(VehicleEntity vehicle) {
|
||
+ VehicleMotionConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.vehicleMotion;
|
||
+ if (!cfg.enabled) return false;
|
||
|
||
+ if (vehicle instanceof MinecartChest && cfg.minecarts && cfg.removeMineshaftChestCarts) {
|
||
+ // Chest-minecarts from mineshafts: just remove, they're not player-placed
|
||
+ vehicle.remove(net.minecraft.world.entity.Entity.RemovalReason.DISCARDED);
|
||
+ return true;
|
||
+ }
|
||
|
||
+ VehicleEntity replacement = createReplacement(vehicle, cfg);
|
||
+ if (replacement == null) return false;
|
||
|
||
+ replacement.setSilent(true);
|
||
+ copyLocation(vehicle, replacement);
|
||
+ copyInventory(vehicle, replacement);
|
||
|
||
+ Level level = vehicle.level();
|
||
+ vehicle.remove(net.minecraft.world.entity.Entity.RemovalReason.CHANGED_DIMENSION);
|
||
+ level.addFreshEntity(replacement);
|
||
+ return true;
|
||
+ }
|
||
|
||
+ private static VehicleEntity createReplacement(VehicleEntity e, VehicleMotionConfig cfg) {
|
||
+ Level lvl = e.level();
|
||
+ if (!cfg.boats && (e instanceof Boat || e instanceof Raft)) return null;
|
||
+ if (!cfg.minecarts && e instanceof AbstractMinecart) return null;
|
||
|
||
+ // Map each concrete type to a "silent" subclass or just return the same type silenced
|
||
+ // In Luminol we achieve the "optimized" variant simply by silencing + disabling idle sounds
|
||
+ // A true replacement only occurs for chest-minecarts (removed above) or via setSilent.
|
||
+ // This mirrors the LagFixer approach of swapping to a VehicleWrapper.
|
||
+ if (e instanceof Minecart) return new Minecart(lvl, e.getX(), e.getY(), e.getZ(), false);
|
||
+ if (e instanceof MinecartHopper) return new MinecartHopper(lvl, e.getX(), e.getY(), e.getZ(), false);
|
||
+ if (e instanceof MinecartFurnace)return new MinecartFurnace(lvl, e.getX(), e.getY(), e.getZ(), false);
|
||
+ if (e instanceof Raft) return new Raft(lvl, e.getX(), e.getY(), e.getZ());
|
||
+ if (e instanceof Boat) return new Boat(lvl, e.getX(), e.getY(), e.getZ());
|
||
+ return null;
|
||
+ }
|
||
|
||
+ private static void copyInventory(VehicleEntity from, VehicleEntity to) {
|
||
+ if (from instanceof ContainerEntity fc && to instanceof ContainerEntity tc) {
|
||
+ for (int i = 0; i < fc.getContainerSize(); i++) {
|
||
+ ItemStack is = fc.getItem(i);
|
||
+ if (!is.isEmpty()) tc.setItem(i, is.copyAndClear());
|
||
+ }
|
||
+ fc.clearContent();
|
||
+ }
|
||
+ }
|
||
|
||
+ private static void copyLocation(VehicleEntity from, VehicleEntity to) {
|
||
+ to.setPos(from.getX(), from.getY(), from.getZ());
|
||
+ float yaw = Location.normalizeYaw(from.yRotO);
|
||
+ to.setYRot(yaw); to.yRotO = yaw; to.setYHeadRot(yaw);
|
||
+ }
|
||
+}
|
||
diff --git a/me/earthme/luminol/explosion/LuminolExplosionOptimizer.java b/me/earthme/luminol/explosion/LuminolExplosionOptimizer.java
|
||
new file mode 100644
|
||
index 0000000000000000000000000000000000000000..dddddddddddddddddddddddddddddddddddddddd
|
||
--- /dev/null
|
||
+++ b/me/earthme/luminol/explosion/LuminolExplosionOptimizer.java
|
||
@@ -0,0 +1,179 @@
|
||
+package me.earthme.luminol.explosion;
|
||
+
|
||
+import me.earthme.luminol.config.modules.optimizations.ExplosionOptimizerConfig;
|
||
+import net.minecraft.core.BlockPos;
|
||
+import net.minecraft.server.level.ServerLevel;
|
||
+import net.minecraft.world.entity.Entity;
|
||
+import net.minecraft.world.entity.item.PrimedTnt;
|
||
+import net.minecraft.world.entity.monster.Creeper;
|
||
+import net.minecraft.world.entity.monster.WitherSkull;
|
||
+import net.minecraft.world.entity.projectile.LargeFireball;
|
||
+import net.minecraft.world.entity.boss.enderdragon.EnderCrystal;
|
||
+import net.minecraft.sounds.SoundEvents;
|
||
+import net.minecraft.sounds.SoundSource;
|
||
+import net.minecraft.world.phys.AABB;
|
||
+import net.minecraft.world.phys.Vec3;
|
||
+
|
||
+import java.util.Set;
|
||
+import java.util.concurrent.ConcurrentHashMap;
|
||
+
|
||
+/**
|
||
+ * Luminol built-in explosion optimizer.
|
||
+ * Intercepts ServerLevel.explode() to apply yield caps and anti-chain logic.
|
||
+ * Ported from LagFixer ExplosionOptimizerModule (GPL-3.0).
|
||
+ */
|
||
+public final class LuminolExplosionOptimizer {
|
||
+
|
||
+ /** Set of recently exploded block-positions (anti-chain guard). */
|
||
+ private static final Set<Long> RECENT_EXPLOSIONS = ConcurrentHashMap.newKeySet();
|
||
+
|
||
+ private LuminolExplosionOptimizer() {}
|
||
+
|
||
+ /**
|
||
+ * Called from ServerLevel.explode() before the explosion is processed.
|
||
+ *
|
||
+ * @return the (possibly clamped) explosion power, or {@code Float.NaN} to cancel entirely.
|
||
+ */
|
||
+ public static float onExplosion(ServerLevel level, Entity source, double x, double y, double z, float power) {
|
||
+ ExplosionOptimizerConfig cfg = me.earthme.luminol.LuminolConfig.get().optimizations.explosionOptimizer;
|
||
+ if (!cfg.enabled) return power;
|
||
|
||
+ // ── Management: cancel + simulate ──────────────────────────────────
|
||
+ if (cfg.managementEnabled && shouldCancel(source, cfg)) {
|
||
+ simulateEffects(level, source, x, y, z, power, cfg);
|
||
+ return Float.NaN; // signals caller to cancel
|
||
+ }
|
||
|
||
+ // ── Anti-chain ──────────────────────────────────────────────────────
|
||
+ if (cfg.antiChainEnabled && isChain(x, y, z, cfg)) {
|
||
+ return Float.NaN;
|
||
+ }
|
||
+ if (cfg.antiChainEnabled) {
|
||
+ markExplosion(x, y, z, cfg);
|
||
+ }
|
||
|
||
+ // ── Yield limit ─────────────────────────────────────────────────────
|
||
+ if (cfg.yieldLimitEnabled) {
|
||
+ float cap = cfg.yieldLimitDefault;
|
||
+ if (source != null) {
|
||
+ String typeName = source.getType().toString().toUpperCase();
|
||
+ cap = cfg.yieldLimitPerType.getOrDefault(typeName, cap);
|
||
+ }
|
||
+ if (power > cap) power = cap;
|
||
+ }
|
||
|
||
+ return power;
|
||
+ }
|
||
|
||
+ // ── Helpers ─────────────────────────────────────────────────────────────
|
||
+ private static boolean shouldCancel(Entity e, ExplosionOptimizerConfig cfg) {
|
||
+ if (e == null) return cfg.cancelBlockExplosions;
|
||
+ if (e instanceof PrimedTnt) return cfg.cancelTnt;
|
||
+ if (e instanceof Creeper) return cfg.cancelCreepers;
|
||
+ if (e instanceof EnderCrystal) return cfg.cancelCrystals;
|
||
+ if (e instanceof WitherSkull) return cfg.cancelWitherSkulls;
|
||
+ if (e instanceof LargeFireball)return cfg.cancelFireballs;
|
||
+ return false;
|
||
+ }
|
||
|
||
+ private static void simulateEffects(ServerLevel level, Entity source, double x, double y, double z, float power, ExplosionOptimizerConfig cfg) {
|
||
+ if (cfg.simulateSound) {
|
||
+ level.playSound(null, BlockPos.containing(x, y, z),
|
||
+ SoundEvents.GENERIC_EXPLODE, SoundSource.BLOCKS,
|
||
+ cfg.simulateSoundVolume, 1.0f);
|
||
+ }
|
||
+ if (!cfg.simulateDamage && !cfg.simulateKnockback) return;
|
||
|
||
+ double radius = power * 2.0;
|
||
+ double r2 = radius * radius;
|
||
+ Vec3 centre = new Vec3(x, y, z);
|
||
|
||
+ for (Entity e : level.getEntities(source, new AABB(x - radius, y - radius, z - radius,
|
||
+ x + radius, y + radius, z + radius))) {
|
||
+ if (!(e instanceof net.minecraft.world.entity.LivingEntity living)) continue;
|
||
+ Vec3 ep = e.position();
|
||
+ double dist2 = centre.distanceToSqr(ep);
|
||
+ if (dist2 > r2) continue;
|
||
|
||
+ if (cfg.simulateDamage) {
|
||
+ double falloff = (r2 - dist2) / r2;
|
||
+ living.hurt(level.damageSources().explosion(source, source),
|
||
+ (float)(falloff * power * cfg.simulateDamageMultiplier));
|
||
+ }
|
||
+ if (cfg.simulateKnockback && dist2 > 0.01) {
|
||
+ Vec3 dir = ep.subtract(centre);
|
||
+ double invLen;
|
||
+ if (cfg.fastInvSqrt) {
|
||
+ double half = 0.5 * dist2;
|
||
+ long bits = Double.doubleToLongBits(dist2);
|
||
+ bits = 0x5fe6eb50c7b537a9L - (bits >> 1);
|
||
+ double y2 = Double.longBitsToDouble(bits);
|
||
+ invLen = y2 * (1.5 - half * y2 * y2);
|
||
+ } else {
|
||
+ invLen = 1.0 / Math.sqrt(dist2);
|
||
+ }
|
||
+ double falloff = (r2 - dist2) / r2;
|
||
+ double str = falloff * falloff * power * cfg.simulateKnockbackMultiplier;
|
||
+ Vec3 kb = dir.scale(invLen * str);
|
||
+ kb = new Vec3(kb.x, kb.y * 0.3 + 0.2, kb.z);
|
||
+ living.setDeltaMovement(living.getDeltaMovement().add(kb));
|
||
+ }
|
||
+ }
|
||
+ }
|
||
|
||
+ private static boolean isChain(double x, double y, double z, ExplosionOptimizerConfig cfg) {
|
||
+ long key = packPos((int) x, (int) y, (int) z);
|
||
+ return RECENT_EXPLOSIONS.contains(key);
|
||
+ }
|
||
|
||
+ private static void markExplosion(double x, double y, double z, ExplosionOptimizerConfig cfg) {
|
||
+ long key = packPos((int) x, (int) y, (int) z);
|
||
+ RECENT_EXPLOSIONS.add(key);
|
||
+ // Schedule removal after cooldown
|
||
+ java.util.concurrent.Executors.newSingleThreadScheduledExecutor()
|
||
+ .schedule(() -> RECENT_EXPLOSIONS.remove(key),
|
||
+ cfg.chainCooldownMs, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||
+ }
|
||
|
||
+ private static long packPos(int x, int y, int z) {
|
||
+ return BlockPos.asLong(x >> 3, y >> 3, z >> 3);
|
||
+ }
|
||
+}
|
||
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
|
||
index 7777777777777777777777777777777777777777..9999999999999999999999999999999999999999 100644
|
||
--- a/net/minecraft/server/level/ServerLevel.java
|
||
+++ b/net/minecraft/server/level/ServerLevel.java
|
||
@@ -1200,6 +1200,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
|
||
@Override
|
||
public net.minecraft.world.level.Explosion explode(@Nullable Entity source, @Nullable DamageSource damageSource,
|
||
@Nullable ExplosionDamageCalculator calculator, double x, double y, double z, float power, boolean fire, Level.ExplosionInteraction mode) {
|
||
+ // Luminol start - ExplosionOptimizer
|
||
+ if (me.earthme.luminol.LuminolConfig.get().optimizations.explosionOptimizer.enabled) {
|
||
+ float newPower = me.earthme.luminol.explosion.LuminolExplosionOptimizer.onExplosion(this, source, x, y, z, power);
|
||
+ if (Float.isNaN(newPower)) {
|
||
+ // Cancelled – return a dummy explosion with no blocks affected
|
||
+ return new net.minecraft.world.level.Explosion(this, source, damageSource, calculator, x, y, z, 0, fire, mode);
|
||
+ }
|
||
+ power = newPower;
|
||
+ }
|
||
+ // Luminol end - ExplosionOptimizer
|
||
return super.explode(source, damageSource, calculator, x, y, z, power, fire, mode);
|
||
}
|
||
@@ -305,6 +305,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
|
||
@Override
|
||
public boolean addFreshEntity(net.minecraft.world.entity.Entity entity) {
|
||
+ // Luminol start - VehicleMotionOptimizer
|
||
+ if (me.earthme.luminol.LuminolConfig.get().optimizations.vehicleMotion.enabled
|
||
+ && entity instanceof net.minecraft.world.entity.vehicle.VehicleEntity ve) {
|
||
+ if (me.earthme.luminol.vehicle.LuminolVehicleOptimizer.tryOptimize(ve)) {
|
||
+ return false; // replaced; original discarded inside tryOptimize
|
||
+ }
|
||
+ }
|
||
+ // Luminol end - VehicleMotionOptimizer
|
||
return super.addFreshEntity(entity);
|
||
}
|