Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for new minecart physics #31

Merged
merged 17 commits into from
Jan 4, 2025
6 changes: 3 additions & 3 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 34 additions & 0 deletions src/main/java/net/pcal/highspeed/HighspeedClientService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}
22 changes: 16 additions & 6 deletions src/main/java/net/pcal/highspeed/HighspeedConfig.java
Original file line number Diff line number Diff line change
@@ -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<HighspeedBlockConfig> blockConfigs,
PerBlockConfig defaultBlockConfig,
Map<ResourceLocation, PerBlockConfig> 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
) {

}
}
156 changes: 122 additions & 34 deletions src/main/java/net/pcal/highspeed/HighspeedConfigParser.java
Original file line number Diff line number Diff line change
@@ -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<HighspeedBlockConfig> 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<ResourceLocation, PerBlockConfig> perBlockConfigs;
{
if (configGson.blocks != null) {
final ImmutableMap.Builder<ResourceLocation, PerBlockConfig> pbcs = ImmutableMap.builder();
configGson.blocks.forEach(bcg -> {
final Collection<String> 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> T elvis(T first, T second) {
return first != null ? first : second;
}

// ===================================================================================
// Gson object model

public static class HighspeedConfigGson {
List<HighspeedBlockConfigGson> blocks;
PerBlockConfigGson defaults;
List<PerBlockConfigGson> 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<String> 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
}
}
Loading
Loading