diff --git a/build.gradle b/build.gradle
index bfa7d4c..28bdf67 100644
--- a/build.gradle
+++ b/build.gradle
@@ -84,6 +84,10 @@ dependencies {
compileOnly fg.deobf("blank:WariumVS:1.1.7")
compileOnly fg.deobf("blank:futurism:0.0.4")
+ // GeckoLib 4.8.4, which Warium VS animates its control surfaces with (optional dependency, remapped, the surface animation mixin
+ // only applies when it and Warium VS are present)
+ compileOnly fg.deobf("maven.modrinth:geckolib:aC5KMoNg")
+
// AzureLib 3.2.7 for the animated block models (optional dependency, the common jar uses obfuscated names, so it is remapped)
compileOnly fg.deobf("mod.azure.azurelib:azurelib-common-${minecraft_version}:${azurelib_version}")
}
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/Config.java b/src/main/java/dev/lumareth/improvedAerodynamics/Config.java
index 0f5d785..e1ae0e0 100644
--- a/src/main/java/dev/lumareth/improvedAerodynamics/Config.java
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/Config.java
@@ -220,6 +220,11 @@ public final class Config {
.comment("Vectored afterburner nozzle: afterburner ignition and heavy afterburner sound")
.define("soundEnabled", true);
+ public static final ForgeConfigSpec.BooleanValue ANIMATE_SURFACES = CLIENT_BUILDER
+ .comment("Control surfaces and flaps visibly deflect by as much as the flight physics deflects them, instead of Warium's",
+ "fixed 20 degree steps. Needs physicsRateAerodynamics on the server")
+ .define("animateControlSurfaces", true);
+
static final ForgeConfigSpec CLIENT_SPEC = CLIENT_BUILDER.build();
private Config() {
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java b/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java
index 28d0d64..f984cbb 100644
--- a/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java
@@ -29,6 +29,7 @@ public class ImprovedAerodynamics {
MinecraftForge.EVENT_BUS.register(ExhaustSync.class);
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.ControlMode.class);
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.FlapInteraction.class);
+ MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.DeflectionSync.class);
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
// Own file name: the default one is taken by Config.CLIENT_SPEC
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/client/animation/SurfaceAnimation.java b/src/main/java/dev/lumareth/improvedAerodynamics/client/animation/SurfaceAnimation.java
new file mode 100644
index 0000000..5e08033
--- /dev/null
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/client/animation/SurfaceAnimation.java
@@ -0,0 +1,84 @@
+package dev.lumareth.improvedAerodynamics.client.animation;
+
+import dev.lumareth.improvedAerodynamics.Config;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import software.bernie.geckolib.core.animatable.model.CoreGeoBone;
+import software.bernie.geckolib.model.GeoModel;
+
+/**
+ * Client side of the control surface animation. The server tells the clients how far each control surface is deflected
+ * (see {@code DeflectionSync}); this class keeps those values, eases towards them, and sets the rotation of the "Fin"
+ * bone of Warium's control surface models to them after GeckoLib has played Warium's own fixed animation.
+ *
+ * Warium's animations tilt the Fin bone by 20 degrees about X for a positive input, which GeckoLib turns into a
+ * rotation of -20 degrees (it negates X for rotations), and the same sign is used here, so a surface moves the way
+ * Warium's own animation moves it, only by the amount the physics uses.
+ */
+@Mod.EventBusSubscriber(modid = dev.lumareth.improvedAerodynamics.ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
+public final class SurfaceAnimation {
+ private static final String BONE = "Fin";
+ // A value that has not been refreshed for this long is dropped, Warium's own animation takes over again
+ private static final long STALE_NANOS = 3_000_000_000L;
+ // Time in seconds the visible deflection takes to close most of the way to the latest value
+ private static final double EASE_SECONDS = 0.07;
+
+ private static final class Entry {
+ float target;
+ float shown;
+ long updated;
+ long drawn;
+ boolean placed;
+ }
+
+ private static final Long2ObjectOpenHashMap ENTRIES = new Long2ObjectOpenHashMap<>();
+
+ private SurfaceAnimation() {
+ }
+
+ /** New deflection of a control surface in degrees, in Warium's animation sign (positive is its "input up" tilt). */
+ public static void set(long pos, float degrees) {
+ Entry entry = ENTRIES.computeIfAbsent(pos, key -> new Entry());
+ entry.target = degrees;
+ entry.updated = System.nanoTime();
+ if (!entry.placed) {
+ // First value of a surface: show it right away, it may have been there before the player came near
+ entry.shown = degrees;
+ entry.placed = true;
+ }
+ }
+
+ public static void apply(BlockEntity blockEntity, GeoModel> model) {
+ if (ENTRIES.isEmpty() || !Config.ANIMATE_SURFACES.get()) {
+ return;
+ }
+ Entry entry = ENTRIES.get(blockEntity.getBlockPos().asLong());
+ if (entry == null) {
+ return;
+ }
+ long now = System.nanoTime();
+ if (now - entry.updated > STALE_NANOS) {
+ ENTRIES.remove(blockEntity.getBlockPos().asLong());
+ return;
+ }
+ // The same block can be drawn more than once a frame (shadows, other passes), the easing steps once per frame
+ if (entry.drawn != 0L && now != entry.drawn) {
+ double dt = (now - entry.drawn) / 1.0E9;
+ entry.shown += (entry.target - entry.shown) * (float) (1.0 - Math.exp(-dt / EASE_SECONDS));
+ }
+ entry.drawn = now;
+ CoreGeoBone bone = model.getAnimationProcessor().getBone(BONE);
+ if (bone != null) {
+ bone.setRotX((float) -Math.toRadians(entry.shown));
+ }
+ }
+
+ @SubscribeEvent
+ public static void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) {
+ ENTRIES.clear();
+ }
+}
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroState.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroState.java
index 8505cbf..9c08e43 100644
--- a/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroState.java
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroState.java
@@ -42,6 +42,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
private boolean[] stalled = new boolean[0];
private boolean wingStalled;
private final Map nodeStates = new ConcurrentHashMap<>();
+ // Every ship's node states by node position, for the control surface animation on the server thread
+ private static final Map ALL_NODE_STATES = new ConcurrentHashMap<>();
private final Wrench wrench = new Wrench();
private final Wrench probe = new Wrench();
@@ -57,6 +59,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
double trimOutput;
// Deflection of this node's flaps in radians, moves towards what the flap lever and airspeed allow
double flapAngle;
+ // Wall clock time of the last physics substep that ran this node, to tell whether its ship is flying
+ volatile long updatedMillis;
// Total pitch input the flight computer applied last substep, pilot and trim together
double computerOutput;
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
@@ -209,9 +213,15 @@ public final class AeroState implements PhysicsBridge.Publishable {
for (int i = 0; i < stalled.length; i++) {
stalled[i] = previous.get(frame.surfaces[i].key());
}
- nodeStates.keySet().removeIf(key -> !frame.nodes.containsKey((long) key));
+ nodeStates.keySet().removeIf(key -> {
+ boolean gone = !frame.nodes.containsKey((long) key);
+ if (gone) {
+ ALL_NODE_STATES.remove(key);
+ }
+ return gone;
+ });
for (NodeData node : frame.nodes.values()) {
- nodeStates.computeIfAbsent(node.key(), key -> new NodeState(node));
+ ALL_NODE_STATES.put(node.key(), nodeStates.computeIfAbsent(node.key(), key -> new NodeState(node)));
}
current = frame;
}
@@ -333,6 +343,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
state.roll = moveTowards(state.roll, effective.roll(), step);
updateFlaps(state, node, worldVelocity.length(), dt);
+ state.updatedMillis = System.currentTimeMillis();
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, torque, dt, improved && wingStalled);
}
}
@@ -351,6 +362,11 @@ public final class AeroState implements PhysicsBridge.Publishable {
state.flapAngle = moveTowards(state.flapAngle, target, Math.toRadians(Config.FLAP_SLEW_DEGREES.get()) * dt);
}
+ /** The physics state of a control node, null if no physics runs for it. */
+ static NodeState nodeState(long nodeKey) {
+ return ALL_NODE_STATES.get(nodeKey);
+ }
+
static double moveTowards(double value, double target, double maxStep) {
if (Math.abs(target - value) <= maxStep) {
return target;
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/DeflectionSync.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/DeflectionSync.java
new file mode 100644
index 0000000..94c24f3
--- /dev/null
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/DeflectionSync.java
@@ -0,0 +1,154 @@
+package dev.lumareth.improvedAerodynamics.flight;
+
+import dev.lumareth.improvedAerodynamics.Config;
+import dev.lumareth.improvedAerodynamics.network.DeflectionPacket;
+import dev.lumareth.improvedAerodynamics.network.Network;
+import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.shorts.ShortArrayList;
+import net.minecraft.core.BlockPos;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.resources.ResourceKey;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraftforge.event.TickEvent;
+import net.minecraftforge.event.server.ServerStoppedEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.network.PacketDistributor;
+import org.joml.Vector3d;
+import org.valkyrienskies.core.api.ships.Ship;
+import org.valkyrienskies.mod.common.VSGameUtilsKt;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tells the clients near a ship how far its control surfaces and flaps are deflected, so they can show it, see
+ * {@code SurfaceAnimation}. Every tick each Warium control surface block reports its deflection from the procedure that
+ * runs it ({@link WariumCapture#begin}); at the end of the tick what changed goes out, in one packet per ship, and
+ * everything again once a second so players who arrive later get it too.
+ *
+ * The deflection is what the physics uses: the smoothed pilot input plus trim and flap angle of the surface's control
+ * node. While the ship is not flying (no physics, nothing is moving) the node's raw input and flap lever are used, and the
+ * flaps are moved towards their angle here, at the configured speed.
+ */
+public final class DeflectionSync {
+ /** Degrees of deflection the physics gives a surface at full input, see ImprovedAero.MAX_DEFLECTION. */
+ private static final double FULL_INPUT_DEGREES = 20.0;
+ private static final long RESEND_TICKS = 20;
+ private static final double RANGE = 256.0;
+ // A node state the physics thread has not touched for this long is not flying any more
+ private static final long FRESH_MILLIS = 500;
+
+ private record Batch(ServerLevel level, double x, double y, double z, LongArrayList positions, ShortArrayList tenths) {
+ }
+
+ private static final Map, Long2ShortOpenHashMap> LAST_VALUE = new HashMap<>();
+ private static final Map, Long2LongOpenHashMap> LAST_SENT = new HashMap<>();
+ // Flap angle (radians) shown while a node is not flying, and the tick it was last moved
+ private static final Map STATIC_FLAPS = new HashMap<>();
+ private static final Map BATCHES = new HashMap<>();
+
+ private DeflectionSync() {
+ }
+
+ /** Called by the procedure of every control surface block each tick, before it adds its forces. */
+ static void report(ServerLevel level, BlockPos pos, SurfaceType type) {
+ if (!type.controllable) {
+ return;
+ }
+ BlockEntity blockEntity = level.getBlockEntity(pos);
+ if (blockEntity == null) {
+ return;
+ }
+ CompoundTag tag = blockEntity.getPersistentData();
+ BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ"));
+ BlockEntity nodeEntity = level.getBlockEntity(nodePos);
+ if (nodeEntity == null || !tag.contains("ControlX")) {
+ return;
+ }
+ long nodeKey = nodePos.asLong();
+ CompoundTag nodeTag = nodeEntity.getPersistentData();
+ AeroState.NodeState state = AeroState.nodeState(nodeKey);
+ boolean flying = state != null && System.currentTimeMillis() - state.updatedMillis < FRESH_MILLIS;
+
+ double input = axis(tag.getString("Key"), nodeTag, state, flying) + axis(tag.getString("Key2"), nodeTag, state, flying);
+ double degrees = FULL_INPUT_DEGREES * Math.max(-1.0, Math.min(1.0, input));
+ if (type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(WariumCapture.FLAP_TAG)) {
+ degrees += Math.toDegrees(flying ? state.flapAngle : staticFlap(level, nodeKey, nodeTag));
+ }
+
+ short tenths = (short) Math.round(Math.max(-90.0, Math.min(90.0, degrees)) * 10.0);
+ ResourceKey dimension = level.dimension();
+ Long2ShortOpenHashMap lastValue = LAST_VALUE.computeIfAbsent(dimension, key -> new Long2ShortOpenHashMap());
+ Long2LongOpenHashMap lastSent = LAST_SENT.computeIfAbsent(dimension, key -> new Long2LongOpenHashMap());
+ long posKey = pos.asLong();
+ long now = level.getGameTime();
+ if (lastSent.containsKey(posKey) && lastValue.get(posKey) == tenths && now - lastSent.get(posKey) < RESEND_TICKS) {
+ return;
+ }
+ lastValue.put(posKey, tenths);
+ lastSent.put(posKey, now);
+
+ Ship ship = VSGameUtilsKt.getShipManagingPos(level, pos);
+ long shipId = ship != null ? ship.getId() : Long.MIN_VALUE;
+ Batch batch = BATCHES.get(shipId);
+ if (batch == null) {
+ Vector3d world = new Vector3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
+ if (ship != null) {
+ ship.getShipToWorld().transformPosition(world);
+ }
+ batch = new Batch(level, world.x, world.y, world.z, new LongArrayList(), new ShortArrayList());
+ BATCHES.put(shipId, batch);
+ }
+ batch.positions.add(posKey);
+ batch.tenths.add(tenths);
+ }
+
+ /** One key's share of a surface's input, like Warium reads "Pitch+" / "Roll-". */
+ private static double axis(String key, CompoundTag nodeTag, AeroState.NodeState state, boolean flying) {
+ ControlAxis axis = ControlAxis.of(key);
+ double multiplier = ControlAxis.multiplier(key);
+ return switch (axis) {
+ case PITCH -> (flying ? state.pitch + state.trimOutput : nodeTag.getDouble("Pitch")) * multiplier;
+ case YAW -> (flying ? state.yaw : nodeTag.getDouble("Yaw")) * multiplier;
+ case ROLL -> (flying ? state.roll : nodeTag.getDouble("Roll")) * multiplier;
+ default -> 0.0;
+ };
+ }
+
+ /** Flap angle of a node that is not flying: moves towards the lever's angle, once per tick however many flaps ask. */
+ private static double staticFlap(ServerLevel level, long nodeKey, CompoundTag nodeTag) {
+ double target = Math.toRadians(FlapSetting.of(nodeTag.getInt(WariumCapture.FLAP_SETTING_TAG)).degrees());
+ double[] entry = STATIC_FLAPS.computeIfAbsent(nodeKey, key -> new double[]{target, Double.NaN});
+ long now = level.getGameTime();
+ if (entry[1] != now) {
+ entry[1] = now;
+ entry[0] = AeroState.moveTowards(entry[0], target, Math.toRadians(Config.FLAP_SLEW_DEGREES.get()) / 20.0);
+ }
+ return entry[0];
+ }
+
+ @SubscribeEvent
+ public static void onServerTick(TickEvent.ServerTickEvent event) {
+ if (event.phase != TickEvent.Phase.END || BATCHES.isEmpty()) {
+ return;
+ }
+ for (Batch batch : BATCHES.values()) {
+ DeflectionPacket packet = new DeflectionPacket(batch.positions.toLongArray(), batch.tenths.toShortArray());
+ Network.CHANNEL.send(PacketDistributor.NEAR.with(() -> new PacketDistributor.TargetPoint(batch.x, batch.y, batch.z,
+ RANGE, batch.level.dimension())), packet);
+ }
+ BATCHES.clear();
+ }
+
+ @SubscribeEvent
+ public static void onServerStopped(ServerStoppedEvent event) {
+ BATCHES.clear();
+ LAST_VALUE.clear();
+ LAST_SENT.clear();
+ STATIC_FLAPS.clear();
+ }
+}
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/MixinPlugin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/MixinPlugin.java
index 9dce857..a86da77 100644
--- a/src/main/java/dev/lumareth/improvedAerodynamics/flight/MixinPlugin.java
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/MixinPlugin.java
@@ -23,11 +23,13 @@ public final class MixinPlugin implements IMixinConfigPlugin {
private boolean warium;
private boolean futurism;
+ private boolean geckolib;
@Override
public void onLoad(String mixinPackage) {
warium = supported("valkyrien_warium", WARIUM_VERSION, "Warium VS");
futurism = supported("futurism", FUTURISM_VERSION, "Futurism");
+ geckolib = LoadingModList.get().getModFileById("geckolib") != null;
}
private static boolean supported(String modId, String version, String name) {
@@ -49,6 +51,10 @@ public final class MixinPlugin implements IMixinConfigPlugin {
if (mixinClassName.contains(".mixin.warium.")) {
return warium;
}
+ if (mixinClassName.contains(".mixin.gecko.")) {
+ // Animates Warium's control surfaces, which are GeckoLib models
+ return warium && geckolib;
+ }
if (mixinClassName.contains(".mixin.futurism.")) {
return futurism;
}
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumCapture.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumCapture.java
index 4c57b67..a3a7401 100644
--- a/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumCapture.java
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumCapture.java
@@ -50,6 +50,9 @@ public final class WariumCapture {
world = level;
pos = BlockPos.containing(x, y, z);
state = blockState;
+ if (surfaceType.controllable && level instanceof ServerLevel serverLevel && Config.PHYSICS_RATE_AERODYNAMICS.get()) {
+ DeflectionSync.report(serverLevel, pos, surfaceType);
+ }
}
/**
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/gecko/GeoModelMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/gecko/GeoModelMixin.java
new file mode 100644
index 0000000..fe666bb
--- /dev/null
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/gecko/GeoModelMixin.java
@@ -0,0 +1,25 @@
+package dev.lumareth.improvedAerodynamics.flight.mixin.gecko;
+
+import dev.lumareth.improvedAerodynamics.client.animation.SurfaceAnimation;
+import net.minecraft.world.level.block.entity.BlockEntity;
+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 software.bernie.geckolib.core.animatable.GeoAnimatable;
+import software.bernie.geckolib.core.animation.AnimationState;
+import software.bernie.geckolib.model.GeoModel;
+
+/**
+ * Runs after GeckoLib has played an animatable's animations, to put Warium's control surfaces at the deflection the
+ * flight physics gives them, see {@link SurfaceAnimation}.
+ */
+@Mixin(value = GeoModel.class, remap = false)
+public abstract class GeoModelMixin {
+ @Inject(method = "handleAnimations", at = @At("RETURN"))
+ private void improvedAerodynamics$deflectSurface(GeoAnimatable animatable, long instanceId, AnimationState animationState, CallbackInfo ci) {
+ if (animatable instanceof BlockEntity blockEntity) {
+ SurfaceAnimation.apply(blockEntity, (GeoModel>) (Object) this);
+ }
+ }
+}
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/network/DeflectionPacket.java b/src/main/java/dev/lumareth/improvedAerodynamics/network/DeflectionPacket.java
new file mode 100644
index 0000000..c7fae36
--- /dev/null
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/network/DeflectionPacket.java
@@ -0,0 +1,50 @@
+package dev.lumareth.improvedAerodynamics.network;
+
+import dev.lumareth.improvedAerodynamics.client.animation.SurfaceAnimation;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.fml.DistExecutor;
+import net.minecraftforge.network.NetworkEvent;
+
+import java.util.function.Supplier;
+
+/** How far control surfaces are deflected, for the clients near a ship: block positions and tenths of a degree. */
+public final class DeflectionPacket {
+ private final long[] positions;
+ private final short[] tenths;
+
+ public DeflectionPacket(long[] positions, short[] tenths) {
+ this.positions = positions;
+ this.tenths = tenths;
+ }
+
+ public void encode(FriendlyByteBuf buffer) {
+ buffer.writeVarInt(positions.length);
+ for (int i = 0; i < positions.length; i++) {
+ buffer.writeLong(positions[i]);
+ buffer.writeShort(tenths[i]);
+ }
+ }
+
+ public static DeflectionPacket decode(FriendlyByteBuf buffer) {
+ int count = buffer.readVarInt();
+ long[] positions = new long[count];
+ short[] tenths = new short[count];
+ for (int i = 0; i < count; i++) {
+ positions[i] = buffer.readLong();
+ tenths[i] = buffer.readShort();
+ }
+ return new DeflectionPacket(positions, tenths);
+ }
+
+ public void handle(Supplier context) {
+ long[] pos = positions;
+ short[] values = tenths;
+ DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
+ for (int i = 0; i < pos.length; i++) {
+ SurfaceAnimation.set(pos[i], values[i] / 10.0F);
+ }
+ });
+ context.get().setPacketHandled(true);
+ }
+}
diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/network/Network.java b/src/main/java/dev/lumareth/improvedAerodynamics/network/Network.java
index 5941690..dfcf0ab 100644
--- a/src/main/java/dev/lumareth/improvedAerodynamics/network/Network.java
+++ b/src/main/java/dev/lumareth/improvedAerodynamics/network/Network.java
@@ -44,6 +44,11 @@ public final class Network {
.decoder(ControlModePacket::decode)
.consumerMainThread(ControlModePacket::handle)
.add();
+ CHANNEL.messageBuilder(DeflectionPacket.class, 11, NetworkDirection.PLAY_TO_CLIENT)
+ .encoder(DeflectionPacket::encode)
+ .decoder(DeflectionPacket::decode)
+ .consumerMainThread(DeflectionPacket::handle)
+ .add();
// Higher id than the neighbours so it does not collide with packets added on other branches
CHANNEL.messageBuilder(FlapPacket.class, 10, NetworkDirection.PLAY_TO_SERVER)
.encoder(FlapPacket::encode)
diff --git a/src/main/resources/improved_aerodynamics.mixins.json b/src/main/resources/improved_aerodynamics.mixins.json
index b56432d..39940dc 100644
--- a/src/main/resources/improved_aerodynamics.mixins.json
+++ b/src/main/resources/improved_aerodynamics.mixins.json
@@ -6,6 +6,7 @@
"compatibilityLevel": "JAVA_17",
"mixins": [
"futurism.ForceInducedShipsMixin",
+ "gecko.GeoModelMixin",
"warium.AirBrakeTickProcedureMixin",
"warium.BypassedCompressorScriptProcedureMixin",
"warium.ControlSurfaceTickProcedureMixin",