diff --git a/gradle.properties b/gradle.properties index 139fab3..4d2b683 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,11 +2,11 @@ org.gradle.jvmargs=-Xmx1G # Mod -mod_version = 0.11.1+1.21.3-prerelease +mod_version = 0.12.0+1.21.3-prerelease maven_group = net.pcal archives_base_name = highspeed-rail # Fabric & Minecraft - https://fabricmc.net/develop minecraft_version=1.21.3 -loader_version=0.16.8 -fabric_version=0.106.1+1.21.3 +loader_version=0.16.9 +fabric_version=0.114.0+1.21.3 \ No newline at end of file diff --git a/src/main/java/net/pcal/highspeed/HighspeedClientService.java b/src/main/java/net/pcal/highspeed/HighspeedClientService.java index 47c99fd..be9731e 100644 --- a/src/main/java/net/pcal/highspeed/HighspeedClientService.java +++ b/src/main/java/net/pcal/highspeed/HighspeedClientService.java @@ -5,12 +5,16 @@ import net.minecraft.client.player.LocalPlayer; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.NewMinecartBehavior; import net.minecraft.world.phys.Vec3; import static java.util.Objects.requireNonNull; public class HighspeedClientService implements ClientModInitializer { + private static final int SPEEDOMETER_REFRESH_FREQUENCY = 4; // in ticks + private int speedometerRefresh = 0; + @Override public void onInitializeClient() { requireNonNull(HighspeedService.getInstance()).initClientService(this); @@ -30,4 +34,34 @@ public void sendPlayerMessage(String message) { final LocalPlayer player = requireNonNull(Minecraft.getInstance().player); player.displayClientMessage(Component.literal(message), true); } + + public void updateSpeedometer(final NewMinecartBehavior nmb, final AbstractMinecart minecart) { + if (!this.isPlayerRiding(minecart)) return; + if (speedometerRefresh++ < SPEEDOMETER_REFRESH_FREQUENCY) return; + speedometerRefresh = 0; + double distanceTraveled = 0; + if (nmb.oldLerp != null && nmb.currentLerpSteps.size() > 2) { + // iterate through the lerp steps to accurately calculate the distance travelled + for (int i = 0; i < nmb.currentLerpSteps.size(); i++) { + if (i == 0) { + distanceTraveled += nmb.oldLerp.position().distanceTo(nmb.currentLerpSteps.get(i).position()); + } else { + distanceTraveled += nmb.currentLerpSteps.get(i - 1).position().distanceTo(nmb.currentLerpSteps.get(i).position()); + } + } + } + final int TICKS_PER_LERP = 3; //?? + double speed = distanceTraveled * 20 / TICKS_PER_LERP; + final String display; + // FIXME I don't understand what is going on with subtracting 1.20 here, but doing so makes it always align with + // the nominal maxSpeed. + speed = Math.max(0, speed - 1.20); + if (speed < 10) { + display = String.format("| %.1f bps |", speed);//, nmb.currentLerpSteps.size(), nmb.currentLerpStepsTotalWeight); + } else { + // not entirely clear why the -1 is necesary here but it makes the top speed line up with the maxSpeed + display = String.format("| %d bps |", Math.round(speed)); //, nmb.currentLerpSteps.size(), nmb.currentLerpStepsTotalWeight); + } + this.sendPlayerMessage(display); + } } diff --git a/src/main/java/net/pcal/highspeed/HighspeedConfig.java b/src/main/java/net/pcal/highspeed/HighspeedConfig.java index 0db389f..cf7fb3f 100644 --- a/src/main/java/net/pcal/highspeed/HighspeedConfig.java +++ b/src/main/java/net/pcal/highspeed/HighspeedConfig.java @@ -1,19 +1,29 @@ package net.pcal.highspeed; -import java.util.List; +import java.util.Map; + import net.minecraft.resources.ResourceLocation; public record HighspeedConfig( - List blockConfigs, + PerBlockConfig defaultBlockConfig, + Map blockConfigs, boolean isSpeedometerEnabled, boolean isTrueSpeedometerEnabled, boolean isIceBoatsEnabled, - Integer defaultSpeedLimit + boolean isNewMinecartPhysicsForceEnabled ) { - public record HighspeedBlockConfig( - ResourceLocation blockId, - Integer speedLimit + public record PerBlockConfig( + Integer oldMaxSpeed, + Integer maxSpeed, + Double boostFactor, + Double boostSlowFactor, + Double boostSlowThreshold, + Double haltThreshold, + Double haltFactor, + Double slowdownFactorOccupied, + Double slowdownFactorEmpty ) { + } } diff --git a/src/main/java/net/pcal/highspeed/HighspeedConfigParser.java b/src/main/java/net/pcal/highspeed/HighspeedConfigParser.java index 722110e..9824224 100644 --- a/src/main/java/net/pcal/highspeed/HighspeedConfigParser.java +++ b/src/main/java/net/pcal/highspeed/HighspeedConfigParser.java @@ -1,71 +1,159 @@ package net.pcal.highspeed; +import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; import net.minecraft.resources.ResourceLocation; -import net.pcal.highspeed.HighspeedConfig.HighspeedBlockConfig; +import net.pcal.highspeed.HighspeedConfig.PerBlockConfig; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; import java.util.List; +import java.util.Set; import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; class HighspeedConfigParser { static HighspeedConfig parse(final InputStream in) throws IOException { - final List blocks = new ArrayList<>(); - final String rawJson = stripComments(new String(in.readAllBytes(), StandardCharsets.UTF_8)); - final Gson gson = new Gson(); - final HighspeedConfigGson configGson = gson.fromJson(rawJson, HighspeedConfigGson.class); - for (HighspeedBlockConfigGson blockGson : configGson.blocks) { - HighspeedBlockConfig bc = new HighspeedBlockConfig( - ResourceLocation.parse(requireNonNull(blockGson.blockId, "blockId is required")), - blockGson.cartSpeed != null ? blockGson.cartSpeed : blockGson.speedLimit - ); - blocks.add(bc); + final String rawJson = (new String(in.readAllBytes(), StandardCharsets.UTF_8)); + class TypoCatchingJsonReader extends JsonReader { + public TypoCatchingJsonReader(StringReader in) { + super(in); + super.setLenient(true); + } + + @Override + public void skipValue() { + // GSon calls this to silently ignore json keys that don't bind to anything. People then get + // confused about why their configuration isn't fully working. So here we just fail loudly instead. + // Note we don't throw IOException because GSon tries to handle that in a ways that obscures the message. + throw new RuntimeException("Unexpected configuration names at: "+this.getPath()); + } + } + final HighspeedConfigGson configGson = + new Gson().fromJson(new TypoCatchingJsonReader(new StringReader(rawJson)), TypeToken.get(HighspeedConfigGson.class)); + + // load the default config + final PerBlockConfig defaultConfig; + { + if (configGson.defaults != null) { + defaultConfig = createPerBlockConfig(configGson.defaults); + } else if (configGson.defaultSpeedLimit != null) { // legacy support + defaultConfig = new PerBlockConfig(configGson.defaultSpeedLimit, null, null, null, null, null, null, null, null); + } else { + defaultConfig = null; + } + } + + // load the per-block configs + final ImmutableMap perBlockConfigs; + { + if (configGson.blocks != null) { + final ImmutableMap.Builder pbcs = ImmutableMap.builder(); + configGson.blocks.forEach(bcg -> { + final Collection blockIds; + if (bcg.blockIds != null) { + blockIds = bcg.blockIds; + } else if (bcg.blockId != null) { // legacy config support + blockIds = Set.of(bcg.blockId); + } else { + throw new RuntimeException("blockIds must be set in 'blocks' configurations"); + } + for (String blockId : blockIds) { + pbcs.put(ResourceLocation.parse(requireNonNull(blockId, "blockIds must not be null")), + mergeConfigs(defaultConfig, createPerBlockConfig(bcg))); + } + }); + perBlockConfigs = pbcs.build(); + } else { + perBlockConfigs = null; + } } - // adjust logging to configured level + + // assemble the final config object return new HighspeedConfig( - Collections.unmodifiableList(blocks), - requireNonNull(configGson.isSpeedometerEnabled, "isSpeedometerEnabled must be set"), - requireNonNull(configGson.isTrueSpeedometerEnabled, "isTrueSpeedometerEnabled must be set"), - requireNonNull(configGson.isIceBoatsEnabled, "isIceBoatsEnabled must be set"), - configGson.defaultSpeedLimit // may be null + defaultConfig, + perBlockConfigs, + requireNonNullElse(configGson.isSpeedometerEnabled,true), + requireNonNullElse(configGson.isTrueSpeedometerEnabled, false), + requireNonNullElse(configGson.isIceBoatsEnabled, false), + requireNonNullElse(configGson.isNewMinecartPhysicsForceEnabled, false) ); } + // =================================================================================== - // Private methods - - private static String stripComments(String json) throws IOException { - final StringBuilder out = new StringBuilder(); - final BufferedReader br = new BufferedReader(new StringReader(json)); - String line; - while ((line = br.readLine()) != null) { - if (!line.strip().startsWith(("//"))) out.append(line).append('\n'); - } - return out.toString(); + // Private + + private static PerBlockConfig createPerBlockConfig(PerBlockConfigGson blockGson) { + return new PerBlockConfig( + blockGson.oldMaxSpeed, + blockGson.maxSpeed, + blockGson.boostFactor, + blockGson.boostSlowFactor, + blockGson.boostSlowThreshold, + blockGson.haltThreshold, + blockGson.haltFactor, + blockGson.slowdownFactorOccupied, + blockGson.slowdownFactorEmpty + ); + } + + private static PerBlockConfig mergeConfigs(PerBlockConfig base, PerBlockConfig overrides) { + if (base == null) return overrides; + return new PerBlockConfig( + elvis(overrides.oldMaxSpeed(), base.oldMaxSpeed()), + elvis(overrides.maxSpeed(), base.maxSpeed()), + elvis(overrides.boostFactor(), base.boostFactor()), + elvis(overrides.boostSlowFactor(), base.boostSlowFactor()), + elvis(overrides.boostSlowThreshold(), base.boostSlowThreshold()), + elvis(overrides.haltThreshold(), base.haltThreshold()), + elvis(overrides.haltFactor(), base.haltFactor()), + elvis(overrides.slowdownFactorOccupied(), base.slowdownFactorOccupied()), + elvis(overrides.slowdownFactorEmpty(), base.slowdownFactorEmpty()) + ); + } + + private static T elvis(T first, T second) { + return first != null ? first : second; } // =================================================================================== // Gson object model public static class HighspeedConfigGson { - List blocks; + PerBlockConfigGson defaults; + List blocks; Boolean isSpeedometerEnabled; Boolean isTrueSpeedometerEnabled; Boolean isIceBoatsEnabled; + Boolean isNewMinecartPhysicsForceEnabled; + + @Deprecated // supports older configs, use defaults/maxSpeed going forward Integer defaultSpeedLimit; } - public static class HighspeedBlockConfigGson { + public static class PerBlockConfigGson { + List blockIds; + @SerializedName(value = "oldMaxSpeed", alternate = {"cartSpeed", "speedLimit"}) // alternates for backwards compat + Integer oldMaxSpeed; + Integer maxSpeed; + Double boostFactor; + Double boostSlowFactor; + Double boostSlowThreshold; + Double haltThreshold; + Double haltFactor; + Double slowdownFactorOccupied; + Double slowdownFactorEmpty; + + @Deprecated // supports older configs, use blockIds going forward String blockId; - Integer speedLimit; - Integer cartSpeed; // for backward compat } } diff --git a/src/main/java/net/pcal/highspeed/HighspeedService.java b/src/main/java/net/pcal/highspeed/HighspeedService.java index a11d78f..75c6b87 100644 --- a/src/main/java/net/pcal/highspeed/HighspeedService.java +++ b/src/main/java/net/pcal/highspeed/HighspeedService.java @@ -1,7 +1,8 @@ package net.pcal.highspeed; -import com.google.common.collect.ImmutableMap; import net.fabricmc.api.ModInitializer; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceLocation; import java.io.FileInputStream; @@ -10,9 +11,21 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.Map; + +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.NewMinecartBehavior; +import net.minecraft.world.entity.vehicle.OldMinecartBehavior; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.PoweredRailBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.pcal.highspeed.HighspeedConfig.PerBlockConfig; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; public class HighspeedService implements ModInitializer { @@ -22,7 +35,8 @@ public class HighspeedService implements ModInitializer { private static HighspeedService INSTANCE = null; private HighspeedConfig config; private HighspeedClientService clientService; - private Map speedLimitPerBlock; + + private final Logger logger = LogManager.getLogger("HighspeedRail"); public static HighspeedService getInstance() { return requireNonNull(INSTANCE); @@ -55,28 +69,79 @@ public void onInitialize() { throw new RuntimeException(e); } - final ImmutableMap.Builder b = ImmutableMap.builder(); - this.config.blockConfigs().forEach(bc->b.put(bc.blockId(), bc.speedLimit())); - this.speedLimitPerBlock = b.build(); - + if (this.config.isNewMinecartPhysicsForceEnabled()) { + this.logger.warn("Experimental minecart movement is force-enabled. This may cause unexpected behavior."); + } if (INSTANCE != null) throw new IllegalStateException(); INSTANCE = this; } - public void initClientService(HighspeedClientService clientService) { - if (this.clientService != null) throw new IllegalStateException(); - this.clientService = requireNonNull(clientService); - } - // =================================================================================== - // Public methods + // OldMinecartBehavior support /** * @return the maximum speed (in blocks-per-second) that a cart travelling on a rail sitting * on the given block type can travel at. Returns null if the vanilla default should be used. + * + * FIXME this should be a Double. */ - public Integer getSpeedLimit(ResourceLocation blockId) { - return this.speedLimitPerBlock.getOrDefault(blockId, this.config.defaultSpeedLimit()); + public Integer getOldMaxSpeed(OldMinecartBehavior omb, AbstractMinecart minecart, ResourceLocation blockId) { + final PerBlockConfig pbc = this.getPerBlockConfig(minecart); + return pbc == null ? null : pbc.oldMaxSpeed(); + } + + // =================================================================================== + // NewMinecartBehavior support + + public boolean isNewMinecartPhysicsForceEnabled() { + return this.config.isNewMinecartPhysicsForceEnabled(); + } + + public Double getMaxSpeed(NewMinecartBehavior nmb, AbstractMinecart minecart) { + final PerBlockConfig pbc = this.getPerBlockConfig(minecart); + if (pbc == null) return null; + return (double)requireNonNullElse(pbc.maxSpeed(), 20) * (minecart.isInWater() ? (double) 0.5F : (double) 1.0F) / (double) 20.0F; + } + + public Double getSlowdownFactor(NewMinecartBehavior nmb, AbstractMinecart minecart) { + final PerBlockConfig pbc = this.getPerBlockConfig(minecart); + if (pbc == null) return null; + return minecart.isVehicle() ? + requireNonNullElse(pbc.slowdownFactorOccupied(), 0.997) : + requireNonNullElse(pbc.slowdownFactorEmpty(), 0.975); + } + + public Vec3 calculateBoostTrackSpeed(NewMinecartBehavior nmb, AbstractMinecart minecart, Vec3 vec3, BlockPos blockPos, BlockState blockState) { + if (blockState.is(Blocks.POWERED_RAIL) && (Boolean) blockState.getValue(PoweredRailBlock.POWERED)) { + final PerBlockConfig pbc = this.getPerBlockConfig(minecart, blockPos); + if (pbc == null) return null; + if (vec3.length() > requireNonNullElse(pbc.boostSlowThreshold(), 0.01)) { + return vec3.normalize().scale(vec3.length() + requireNonNullElse(pbc.boostFactor(), 0.06)); + } else { + Vec3 vec32 = minecart.getRedstoneDirection(blockPos); + return vec32.lengthSqr() <= (double) 0.0F ? vec3 : vec32.scale(vec3.length() + requireNonNullElse(pbc.boostSlowFactor(), 0.2)); + } + } else { + return vec3; // this would be the vanilla result + } + } + + public Vec3 calculateHaltTrackSpeed(NewMinecartBehavior nmb, AbstractMinecart minecart, Vec3 vec3, BlockState blockState) { + if (blockState.is(Blocks.POWERED_RAIL) && !(Boolean) blockState.getValue(PoweredRailBlock.POWERED)) { + final PerBlockConfig pbc = this.getPerBlockConfig(minecart); + if (pbc == null) return null; + return vec3.length() < requireNonNullElse(pbc.haltThreshold(), 0.03) ? Vec3.ZERO : vec3.scale(requireNonNullElse(pbc.haltFactor(), 0.5)); + } else { + return vec3; + } + } + + // =================================================================================== + // Client-specific support. This need to be a separate mod + + public void initClientService(HighspeedClientService clientService) { + if (this.clientService != null) throw new IllegalStateException(); + this.clientService = requireNonNull(clientService); } public boolean isSpeedometerEnabled() { @@ -95,4 +160,19 @@ public HighspeedClientService getClientService() { if (this.clientService == null) throw new UnsupportedOperationException("clientService not initialized"); return this.clientService; } + + // =================================================================================== + // Private + + private PerBlockConfig getPerBlockConfig(AbstractMinecart minecart) { + return getPerBlockConfig(minecart, minecart.blockPosition()); + } + + private PerBlockConfig getPerBlockConfig(AbstractMinecart minecart, BlockPos minecartPos) { + if (this.config.blockConfigs() == null) return this.config.defaultBlockConfig(); + final BlockState underState = minecart.level().getBlockState(minecartPos.below()); + final ResourceLocation underBlockId = BuiltInRegistries.BLOCK.getKey(underState.getBlock()); + final PerBlockConfig pbc = this.config.blockConfigs().get(underBlockId); + return pbc != null ? pbc : this.config.defaultBlockConfig(); + } } diff --git a/src/main/java/net/pcal/highspeed/mixins/AbstractMinecartMixin.java b/src/main/java/net/pcal/highspeed/mixins/AbstractMinecartMixin.java new file mode 100644 index 0000000..177c96b --- /dev/null +++ b/src/main/java/net/pcal/highspeed/mixins/AbstractMinecartMixin.java @@ -0,0 +1,18 @@ +package net.pcal.highspeed.mixins; + +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.level.Level; +import net.pcal.highspeed.HighspeedService; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(AbstractMinecart.class) +public class AbstractMinecartMixin { + + @Inject(method = "useExperimentalMovement", at = @At("HEAD"), cancellable = true) + private static void mf_useExperimentalMovement(Level level, CallbackInfoReturnable cir) { + if (HighspeedService.getInstance().isNewMinecartPhysicsForceEnabled()) cir.setReturnValue(true); + } +} diff --git a/src/main/java/net/pcal/highspeed/mixins/NewMinecartBehaviorMixin.java b/src/main/java/net/pcal/highspeed/mixins/NewMinecartBehaviorMixin.java new file mode 100644 index 0000000..300bcbd --- /dev/null +++ b/src/main/java/net/pcal/highspeed/mixins/NewMinecartBehaviorMixin.java @@ -0,0 +1,58 @@ +package net.pcal.highspeed.mixins; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.entity.vehicle.NewMinecartBehavior; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.pcal.highspeed.HighspeedService; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(NewMinecartBehavior.class) +public abstract class NewMinecartBehaviorMixin { + + @Inject(method = "tick", at = @At("HEAD")) + public void tick(CallbackInfo ci) { + final AbstractMinecart minecart = ((MinecartBehaviorAccessor) this).getMinecart(); + if (!minecart.level().isClientSide()) return; + final HighspeedService service = HighspeedService.getInstance(); + if (!service.isSpeedometerEnabled()) return; + HighspeedService.getInstance().getClientService().updateSpeedometer((NewMinecartBehavior) (Object)this, minecart); + } + + @Inject(method = "getMaxSpeed", at = @At("HEAD"), cancellable = true) + protected void getMaxSpeed(CallbackInfoReturnable cir) { + final Double customMaxSpeed = HighspeedService.getInstance().getMaxSpeed( + (NewMinecartBehavior) (Object)this, ((MinecartBehaviorAccessor) this).getMinecart() + ); + if (customMaxSpeed != null) cir.setReturnValue(customMaxSpeed); + } + + @Inject(method = "getSlowdownFactor", at = @At("HEAD"), cancellable = true) + protected void getSlowdownFactor(CallbackInfoReturnable cir) { + final Double customSlowdownFactor = HighspeedService.getInstance().getSlowdownFactor( + (NewMinecartBehavior) (Object)this, ((MinecartBehaviorAccessor) this).getMinecart() + ); + if (customSlowdownFactor != null) cir.setReturnValue(customSlowdownFactor); + } + + @Inject(method = "calculateBoostTrackSpeed", at = @At("HEAD"), cancellable = true) + private void calculateBoostTrackSpeed(Vec3 vec3, BlockPos blockPos, BlockState blockState, CallbackInfoReturnable cir) { + final Vec3 customBoostSpeed = HighspeedService.getInstance().calculateBoostTrackSpeed( + (NewMinecartBehavior) (Object)this, ((MinecartBehaviorAccessor) this).getMinecart(), vec3, blockPos, blockState + ); + if (customBoostSpeed != null) cir.setReturnValue(customBoostSpeed); + } + + @Inject(method = "calculateHaltTrackSpeed", at = @At("HEAD"), cancellable = true) + private void calculateHaltTrackSpeed(Vec3 vec3, BlockState blockState, CallbackInfoReturnable cir) { + final Vec3 customHaltSpeed = HighspeedService.getInstance().calculateHaltTrackSpeed( + (NewMinecartBehavior) (Object)this, ((MinecartBehaviorAccessor) this).getMinecart(), vec3, blockState + ); + if (customHaltSpeed != null) cir.setReturnValue(customHaltSpeed); + } +} diff --git a/src/main/java/net/pcal/highspeed/mixins/OldMinecartBehaviorMixin.java b/src/main/java/net/pcal/highspeed/mixins/OldMinecartBehaviorMixin.java index 9b5e447..cb68bad 100644 --- a/src/main/java/net/pcal/highspeed/mixins/OldMinecartBehaviorMixin.java +++ b/src/main/java/net/pcal/highspeed/mixins/OldMinecartBehaviorMixin.java @@ -14,6 +14,7 @@ import net.pcal.highspeed.HighspeedService; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; @@ -23,16 +24,29 @@ @Mixin(OldMinecartBehavior.class) public abstract class OldMinecartBehaviorMixin { + @Unique private static final double VANILLA_MAX_SPEED = 8.0 / 20.0; + + @Unique private static final double SQRT_TWO = 1.414213; + @Unique private BlockPos lastPos = null; + + @Unique private double currentMaxSpeed = VANILLA_MAX_SPEED; + + @Unique private double lastMaxSpeed = VANILLA_MAX_SPEED; + + @Unique private Vec3 lastSpeedPos = null; + + @Unique private long lastSpeedTime = 0; - private final OldMinecartBehavior minecartBehavior = (OldMinecartBehavior) (Object) this; - private final AbstractMinecart minecart = ((MinecartBehaviorAccessor) minecartBehavior).getMinecart(); + + // =================================================================================== + // Mixin methods @Inject(method = "tick", at = @At("HEAD")) public void tick(CallbackInfo ci) { @@ -55,7 +69,12 @@ protected void getMaxSpeed(CallbackInfoReturnable cir) { } } + // =================================================================================== + // Private + + @Unique private double getModifiedMaxSpeed() { + final AbstractMinecart minecart = ((MinecartBehaviorAccessor) this).getMinecart(); final BlockPos currentPos = minecart.blockPosition(); if (currentPos.equals(lastPos)) return currentMaxSpeed; lastPos = currentPos; @@ -74,9 +93,10 @@ private double getModifiedMaxSpeed() { } else { final BlockState underState = minecart.level().getBlockState(currentPos.below()); final ResourceLocation underBlockId = BuiltInRegistries.BLOCK.getKey(underState.getBlock()); - final Integer speedLimit = HighspeedService.getInstance().getSpeedLimit(underBlockId); - if (speedLimit != null) { - return currentMaxSpeed = speedLimit / 20.0; + final Integer maxSpeed = HighspeedService.getInstance().getOldMaxSpeed( + (OldMinecartBehavior) (Object) this, minecart, underBlockId); + if (maxSpeed != null) { + return currentMaxSpeed = maxSpeed / 20.0; } else { return currentMaxSpeed = VANILLA_MAX_SPEED; } @@ -86,9 +106,11 @@ private double getModifiedMaxSpeed() { } } + @Unique private void clampVelocity() { if (getModifiedMaxSpeed() != lastMaxSpeed) { double smaller = Math.min(getModifiedMaxSpeed(), lastMaxSpeed); + final AbstractMinecart minecart = ((MinecartBehaviorAccessor) this).getMinecart(); final Vec3 vel = minecart.getDeltaMovement(); minecart.setDeltaMovement(new Vec3(Mth.clamp(vel.x, -smaller, smaller), 0.0, Mth.clamp(vel.z, -smaller, smaller))); @@ -96,6 +118,7 @@ private void clampVelocity() { lastMaxSpeed = currentMaxSpeed; } + @Unique private void updateSpeedometer() { final HighspeedService service = HighspeedService.getInstance(); if (!service.isSpeedometerEnabled()) return; diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index c8095cf..7433562 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -21,7 +21,7 @@ "highspeed-mixin.json" ], "depends": { - "fabricloader": ">=0.16.8", + "fabricloader": ">=0.16.9", "fabric": "*", "minecraft": ">=1.21.3", "java": ">=21" diff --git a/src/main/resources/highspeed-mixin.json b/src/main/resources/highspeed-mixin.json index e33324a..2ede4ca 100644 --- a/src/main/resources/highspeed-mixin.json +++ b/src/main/resources/highspeed-mixin.json @@ -4,7 +4,9 @@ "package": "net.pcal.highspeed.mixins", "compatibilityLevel": "JAVA_17", "mixins": [ + "AbstractMinecartMixin", "OldMinecartBehaviorMixin", + "NewMinecartBehaviorMixin", "AbstractBoatEntityMixin", "MinecartBehaviorAccessor" ], diff --git a/src/main/resources/net/pcal/highspeed/default-config.json5 b/src/main/resources/net/pcal/highspeed/default-config.json5 index 62c4690..c6955bf 100644 --- a/src/main/resources/net/pcal/highspeed/default-config.json5 +++ b/src/main/resources/net/pcal/highspeed/default-config.json5 @@ -1,40 +1,184 @@ -// NOTE: This is not actually json5 - it's parsed with minecraft's built-in json parser but with -// some preprocessing to allow for '//' -style comments like this one. The lie in the .json5 -// extension just helps text editors deal with it. +/** + * Config file for Highspeed Rail: + * https://modrinth.com/mod/highspeed-rail + * + * This default configuration file sets your world up to have fast minecarts + * when the rails are built on gravel blocks. In normal worlds, minecarts + * on gravel will travel up to 40 blocks-per-second; in worlds with the + * experimental 'Minecart Improvements' enabled, carts on gravel will travel + * at 200 blocks-per-second. + * + * There's a lot you can tweak here, though, so read on. + * + * + * MINECART PHYSICS MODELS + * + * The mod supports both the classic minecart physics model as well as the + * new experimental model introduced in minecraft 1.21.3. Some config + * settings only apply to the new physics model. The mod provides an option + * to enable the new model even in old worlds. + * + * + * EDITING AND MANAGING THIS FILE + * + * That this file is not actually json5 - it's parsed with minecraft's + * built-in json parser in lenient mode, mainly to make it ignore comments + * like this one. The lie with the .json5 extension just helps text editors + * happy. + * + * The default config file will automatically get created during Minecraft + * startup if it doesn't exist. If you get stuck with configuration errors, + * you can start over by renaming this file to 'highspeed-rail.json5.old' + * and then restarting Minecraft. + + * The configuration format sometimes changes with new versions of the mod. + * Reasonable efforts are made to preserve backwards compatibility. But you + * may want to regenerate the default config after updating if you run into + * problems or want to take advantage of new features. + */ + { - // Sets the maximum speed (in blocks-per-second) for carts travelling on specific blocks. The speedLimit in - // Vanilla is 8. + /** + * Setting this to true will force the new minecart physics to be enabled + * for *all* worlds, even those that were created without the experimental + * 'Minecart Improvements' enabled. As of Minecraft 1.24.3, the new physics + * model is experimental and may result in unexpected behavior. + * + * The new physics model is vastly better than the old, and it's pretty + * unlikely that this could cause world corruption, so you'll probably be + * happy with the results of turning this on. Nonetheless, you do so at + * your own risk. + */ + 'isNewMinecartPhysicsForceEnabled' : false, + + /** + * The 'defaults' section allows you to tweak how minecarts behave by + * by default. The values show in the file by default are the vanilla + * values. + * + * These values can also be changed based on the kind of block the minecart + * is travelling on (see the 'blocks' section below). + */ + 'defaults' : { + + /** + * Applies to the OLD physics model only. + * + * Sets the default maximum speed for worlds using the OLD physics model, + * expressed in blocks-per-second. The value must be an integer. + * + * The default vanilla value is 8. The practical upper bound on this + * value is about 78, due to limitations in the old model. + * + * Note that this does not increase the acceleration rate; it only + * increases top speed. + * + * If you change this, you should also configure at least one kind of + * block to maintain Vanilla speed (i.e.,by setting 'speedLimit' for a + * block to be 8 (or null) in the 'blocks' section above). Experience + * shows that carts can derail or reverse unexpectedly in certain + * situations when travelling above the Vanilla speed limit, so you'll + * want to have at least one block type that can act as a 'brake.' + */ + 'oldMaxSpeed' : 8, + + + /** + * Applies to the NEW physics model only. + * + * Sets the default maximum speed for worlds using the NEW physics model, + * expressed in blocks-per-second (bps). The value must be an integer. . + * + * The default vanilla value is 8 but can be changed with the + * 'minecartMaxSpeed' gamerule. This configuration setting overrides + * the gamerule setting. + * + * Unlike the gamerule, which has an upper bound of 1000, this setting + * can be made as high as you want. Experiments have had minecarts + * going in excess of 40,000 bps but have also led to game crashes. + * Exceed 1000 at your own risk. + */ + 'maxSpeed' : 8, + + /** + * Applies to the NEW physics model only. + * + * Sets the scaling factor used to accelerate minecarts on powered rails. + * Roughly speaking, doubling this value will result in doubling the + * acceleration. Changing this may lead to undesirable behavior. + */ + 'boostFactor' : 0.06, + + /** + * Applies to the NEW physics model only. + * + * Sets the scaling factor used to decelerate occupied minecarts. + * Changing this may lead to undesirable behavior. + */ + 'slowdownFactorOccupied' : 0.997, + + /** + * Applies to the NEW physics model only. + * + * Sets the scaling factor used to decelerate unoccupied minecarts. + * Changing this may lead to undesirable behavior. + */ + 'slowdownFactorEmpty' : 0.975 + }, + + /** + * The 'blocks' section allows you to tweak how minecarts behave when + * travelling on top of specific block types. Each item in the blocks'list + * must have a 'blockIds' field and can also have any of the settings + * documented above in the 'defaults' section. + */ 'blocks' : [ { - 'blockId': 'minecraft:gravel', - 'speedLimit': 40 + /** + * A list of ids of the blocks that the settings apply to. In this + * example, we increase the maximum minecart speed for rails that are + * built on top of gravel blocks. + */ + 'blockIds': ['minecraft:gravel'], + 'oldMaxSpeed': 40, + 'maxSpeed': 200 }, { - 'blockId': 'minecraft:slime_block', - 'speedLimit': 4 + 'blockIds': ['minecraft:slime_block'], + 'oldMaxSpeed': 4, + 'maxSpeed' : 4 } ], - // Sets the default maximum speed for blocks not configured above. A value of 'null' here means to use the - // Vanilla speedLimit (8). - // - // If you change this, you should also configure at least one kind of block to maintain Vanilla speed (i.e., - // by setting 'speedLimit' for a block to be 8 (or null) in the 'blocks' section above). Experience shows that - // carts can derail or reverse unexpectedly in certain situations when travelling above the Vanilla speed limit, - // so you'll want to have at least one block type that can act as a 'brake.' - 'defaultSpeedLimit' : null, - - // Whether a speedometer should be displayed when you get in a minecart. The mod must be installed on the client - // in order for this to work. + + /** + * Applies to both the NEW and OLD physics models. Mod must be installed + * on the client. + * + * Whether a speedometer should be displayed when you get in a minecart. + */ 'isSpeedometerEnabled' : true, - // Whether the 'true' speed should also be displayed on the speedometer. The default speedometer just shows your - // approximate speed; this precisely measures distance travelled every tick. It's more resource-intensive - // and also flickers sort of annoyingly, so disabled by default. There's usually not much difference, anyway. + /** + * Applies to both the OLD physics model only. Mod must be installed + * on the client. + * + * Whether the 'true' speed should also be displayed on the speedometer. + * The default speedometer just shows your approximate speed; this precisely + * measures distance travelled every tick. It's more resource-intensive + * and also flickers sort of annoyingly, so disabled by default. There's + * usually not much difference, anyway. + */ 'isTrueSpeedometerEnabled' : false, - // Whether Vanilla ice boats should be allowed. Set to 'false' to limit ice boats to ground speed; 'true' - // The mod must be installed on the client for this to work. + /** + * Applies to both the NEW and OLD physics models. Mod must be installed + * on the client. + * + * Whether Vanilla ice boats should be allowed. Set to 'false' to limit + * ice boats to ground speed + */ 'isIceBoatsEnabled' : false + }