From 009d97158402cf8911a430123d1cf5239b340dfd Mon Sep 17 00:00:00 2001 From: senseiwells <66843746+senseiwells@users.noreply.github.com> Date: Wed, 25 Jan 2023 16:40:18 +0000 Subject: [PATCH] Add rule to make the World Border update with ticks instead of real time (#1550) --- src/main/java/carpet/CarpetSettings.java | 44 +++++ .../carpet/mixins/WorldBorder_tickMixin.java | 25 +++ .../patches/TickSyncedBorderExtent.java | 168 ++++++++++++++++++ src/main/resources/carpet.accesswidener | 4 + src/main/resources/carpet.mixins.json | 1 + 5 files changed, 242 insertions(+) create mode 100644 src/main/java/carpet/mixins/WorldBorder_tickMixin.java create mode 100644 src/main/java/carpet/patches/TickSyncedBorderExtent.java diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java index 1e4849cf..92dc919f 100644 --- a/src/main/java/carpet/CarpetSettings.java +++ b/src/main/java/carpet/CarpetSettings.java @@ -27,6 +27,8 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.StructureBlockEntity; import net.minecraft.world.level.block.piston.PistonStructureResolver; +import net.minecraft.world.level.border.BorderStatus; +import net.minecraft.world.level.border.WorldBorder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1068,6 +1070,48 @@ public class CarpetSettings ) public static int sculkSensorRange = 8; + /** + * Listener, we need to update world borders to change whether + * they are currently moving in real time or in game time. + */ + private static class WorldBorderValidator extends Validator + { + @Override + public Boolean validate(CommandSourceStack source, CarpetRule changingRule, Boolean newValue, String userInput) + { + if (changingRule.value() ^ newValue) + { + // Needed for the update + tickSyncedWorldBorders = newValue; + MinecraftServer server = CarpetServer.minecraft_server; + if (server == null) + { + return newValue; + } + for (ServerLevel level : server.getAllLevels()) + { + WorldBorder worldBorder = level.getWorldBorder(); + if (worldBorder.getStatus() != BorderStatus.STATIONARY) + { + double from = worldBorder.getSize(); + double to = worldBorder.getLerpTarget(); + long time = worldBorder.getLerpRemainingTime(); + worldBorder.lerpSizeBetween(from, to, time); + } + } + } + return newValue; + } + } + + @Rule( + desc = "Makes world borders move based on in game time instead of real time", + extra = "This has the effect that when the tick rate changes the world border speed also changes proportional to it", + category = FEATURE, + validate = WorldBorderValidator.class + ) + public static boolean tickSyncedWorldBorders = false; + public enum FungusFixMode { FALSE, RANDOM, ALL; } diff --git a/src/main/java/carpet/mixins/WorldBorder_tickMixin.java b/src/main/java/carpet/mixins/WorldBorder_tickMixin.java new file mode 100644 index 00000000..fe80ab62 --- /dev/null +++ b/src/main/java/carpet/mixins/WorldBorder_tickMixin.java @@ -0,0 +1,25 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.patches.TickSyncedBorderExtent; +import net.minecraft.world.level.border.WorldBorder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(WorldBorder.class) +public class WorldBorder_tickMixin +{ + @Shadow private WorldBorder.BorderExtent extent; + + @Inject(method = "lerpSizeBetween", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/border/WorldBorder;getListeners()Ljava/util/List;")) + private void getExtent(double d, double e, long l, CallbackInfo ci) + { + if (d != e && CarpetSettings.tickSyncedWorldBorders) + { + this.extent = new TickSyncedBorderExtent((WorldBorder) (Object) this, l, d, e); + } + } +} diff --git a/src/main/java/carpet/patches/TickSyncedBorderExtent.java b/src/main/java/carpet/patches/TickSyncedBorderExtent.java new file mode 100644 index 00000000..f0c87319 --- /dev/null +++ b/src/main/java/carpet/patches/TickSyncedBorderExtent.java @@ -0,0 +1,168 @@ +package carpet.patches; + +import carpet.CarpetServer; +import carpet.helpers.TickSpeed; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.Mth; +import net.minecraft.world.level.border.BorderChangeListener; +import net.minecraft.world.level.border.BorderStatus; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +/** + * This class is essentially a copy of {@link net.minecraft.world.level.border.WorldBorder.MovingBorderExtent} + * but instead of using real time to lerp the border + * this class uses the in game ticks. + */ +@SuppressWarnings("JavadocReference") +public class TickSyncedBorderExtent implements WorldBorder.BorderExtent +{ + private final WorldBorder border; + private final long realDuration; + private final double tickDuration; + private final double from; + private final double to; + + private int ticks; + + public TickSyncedBorderExtent(WorldBorder border, long realDuration, double from, double to) + { + this.border = border; + this.realDuration = realDuration; + this.tickDuration = realDuration / 50.0; + this.from = from; + this.to = to; + this.ticks = 0; + } + + @Override + public double getMinX() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterX() - this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getMaxX() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterX() + this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getMinZ() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterZ() - this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getMaxZ() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterZ() + this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getSize() + { + double progress = this.ticks / this.tickDuration; + return progress < 1.0 ? Mth.lerp(progress, this.from, this.to) : this.to; + } + + @Override + public double getLerpSpeed() + { + return Math.abs(this.from - this.to) / this.realDuration; + } + + @Override + public long getLerpRemainingTime() + { + // Rough estimation + MinecraftServer server = CarpetServer.minecraft_server; + double ms; + if (server == null) + { + ms = TickSpeed.mspt; + } + else + { + ms = Arrays.stream(server.tickTimes).average().orElseThrow(IllegalStateException::new) * 1.0E-6D; + } + double tps = 1_000.0D / Math.max((TickSpeed.time_warp_start_time != 0) ? 0.0 : TickSpeed.mspt, ms); + return (long) ((this.tickDuration - this.ticks) / tps * 1_000); + } + + @Override + public double getLerpTarget() + { + return this.to; + } + + @NotNull + @Override + public BorderStatus getStatus() + { + return this.to < this.from ? BorderStatus.SHRINKING : BorderStatus.GROWING; + } + + @Override + public void onAbsoluteMaxSizeChange() + { + + } + + @Override + public void onCenterChange() + { + + } + + @NotNull + @Override + public WorldBorder.BorderExtent update() + { + if (this.ticks++ % 20 == 0) + { + // We need to update any listeners + // Most importantly those that send updates to the client + // This is because the client logic uses real time + // So if the tick speed has changed we need to tell the client + for (BorderChangeListener listener : this.border.getListeners()) + { + // We do not want to update DelegateBorderChangeListener + // This updates borders in other dimensions + if (!(listener instanceof BorderChangeListener.DelegateBorderChangeListener)) + { + listener.onBorderSizeLerping(this.border, this.from, this.to, this.realDuration); + } + } + } + + return this.ticks >= this.tickDuration ? this.border.new StaticBorderExtent(this.to) : this; + } + + @NotNull + @Override + public VoxelShape getCollisionShape() + { + return Shapes.join( + Shapes.INFINITY, + Shapes.box( + Math.floor(this.getMinX()), + Double.NEGATIVE_INFINITY, + Math.floor(this.getMinZ()), + Math.ceil(this.getMaxX()), + Double.POSITIVE_INFINITY, + Math.ceil(this.getMaxZ()) + ), + BooleanOp.ONLY_FIRST + ); + } +} diff --git a/src/main/resources/carpet.accesswidener b/src/main/resources/carpet.accesswidener index 903db440..e3369021 100644 --- a/src/main/resources/carpet.accesswidener +++ b/src/main/resources/carpet.accesswidener @@ -4,7 +4,11 @@ accessible class net/minecraft/server/level/ChunkMap$DistanceManager accessible class net/minecraft/server/level/ThreadedLevelLightEngine$TaskType accessible class net/minecraft/world/item/crafting/Ingredient$Value accessible class net/minecraft/world/level/lighting/BlockLightSectionStorage$BlockDataLayerStorageMap +accessible class net/minecraft/world/level/border/WorldBorder$BorderExtent +accessible class net/minecraft/world/level/border/WorldBorder$StaticBorderExtent accessible class net/minecraft/server/MinecraftServer$ReloadableResources accessible class net/minecraft/world/level/biome/Biome$ClimateSettings +accessible method net/minecraft/world/level/border/WorldBorder getListeners ()Ljava/util/List; + accessible field net/minecraft/world/level/block/state/BlockBehaviour UPDATE_SHAPE_ORDER [Lnet/minecraft/core/Direction; diff --git a/src/main/resources/carpet.mixins.json b/src/main/resources/carpet.mixins.json index f154e797..aae9683c 100644 --- a/src/main/resources/carpet.mixins.json +++ b/src/main/resources/carpet.mixins.json @@ -38,6 +38,7 @@ "ChunkMap_tickMixin", "DistanceManager_tickMixin", "ServerFunctionManager_tickMixin", + "WorldBorder_tickMixin", "ChunkMap_scarpetChunkCreationMixin", "LevelEntityGetterAdapter_scarpetMixin", "ChunkHolder_scarpetChunkCreationMixin",