Animate control surfaces and flaps from the real deflection
A GeckoLib GeoModel mixin sets the Fin bone of Warium's control surfaces to the deflection the flight physics uses, synced to nearby clients per ship. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
12 files changed
+356
-2
No files matched your search
@@ -84,6 +84,10 @@ dependencies {
|
|||||||
compileOnly fg.deobf("blank:WariumVS:1.1.7")
|
compileOnly fg.deobf("blank:WariumVS:1.1.7")
|
||||||
compileOnly fg.deobf("blank:futurism:0.0.4")
|
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)
|
// 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}")
|
compileOnly fg.deobf("mod.azure.azurelib:azurelib-common-${minecraft_version}:${azurelib_version}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,11 @@ public final class Config {
|
|||||||
.comment("Vectored afterburner nozzle: afterburner ignition and heavy afterburner sound")
|
.comment("Vectored afterburner nozzle: afterburner ignition and heavy afterburner sound")
|
||||||
.define("soundEnabled", true);
|
.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();
|
static final ForgeConfigSpec CLIENT_SPEC = CLIENT_BUILDER.build();
|
||||||
|
|
||||||
private Config() {
|
private Config() {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class ImprovedAerodynamics {
|
|||||||
MinecraftForge.EVENT_BUS.register(ExhaustSync.class);
|
MinecraftForge.EVENT_BUS.register(ExhaustSync.class);
|
||||||
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.ControlMode.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.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.SERVER, Config.SPEC);
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
|
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
|
||||||
// Own file name: the default one is taken by Config.CLIENT_SPEC
|
// Own file name: the default one is taken by Config.CLIENT_SPEC
|
||||||
|
|||||||
+84
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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<Entry> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
private boolean[] stalled = new boolean[0];
|
private boolean[] stalled = new boolean[0];
|
||||||
private boolean wingStalled;
|
private boolean wingStalled;
|
||||||
private final Map<Long, NodeState> nodeStates = new ConcurrentHashMap<>();
|
private final Map<Long, NodeState> nodeStates = new ConcurrentHashMap<>();
|
||||||
|
// Every ship's node states by node position, for the control surface animation on the server thread
|
||||||
|
private static final Map<Long, NodeState> ALL_NODE_STATES = new ConcurrentHashMap<>();
|
||||||
private final Wrench wrench = new Wrench();
|
private final Wrench wrench = new Wrench();
|
||||||
private final Wrench probe = new Wrench();
|
private final Wrench probe = new Wrench();
|
||||||
|
|
||||||
@@ -57,6 +59,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
double trimOutput;
|
double trimOutput;
|
||||||
// Deflection of this node's flaps in radians, moves towards what the flap lever and airspeed allow
|
// Deflection of this node's flaps in radians, moves towards what the flap lever and airspeed allow
|
||||||
double flapAngle;
|
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
|
// Total pitch input the flight computer applied last substep, pilot and trim together
|
||||||
double computerOutput;
|
double computerOutput;
|
||||||
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
|
// 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++) {
|
for (int i = 0; i < stalled.length; i++) {
|
||||||
stalled[i] = previous.get(frame.surfaces[i].key());
|
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()) {
|
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;
|
current = frame;
|
||||||
}
|
}
|
||||||
@@ -333,6 +343,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
||||||
state.roll = moveTowards(state.roll, effective.roll(), step);
|
state.roll = moveTowards(state.roll, effective.roll(), step);
|
||||||
updateFlaps(state, node, worldVelocity.length(), dt);
|
updateFlaps(state, node, worldVelocity.length(), dt);
|
||||||
|
state.updatedMillis = System.currentTimeMillis();
|
||||||
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, torque, dt, improved && wingStalled);
|
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);
|
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) {
|
static double moveTowards(double value, double target, double maxStep) {
|
||||||
if (Math.abs(target - value) <= maxStep) {
|
if (Math.abs(target - value) <= maxStep) {
|
||||||
return target;
|
return target;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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<ResourceKey<Level>, Long2ShortOpenHashMap> LAST_VALUE = new HashMap<>();
|
||||||
|
private static final Map<ResourceKey<Level>, 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<Long, double[]> STATIC_FLAPS = new HashMap<>();
|
||||||
|
private static final Map<Long, Batch> 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<Level> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,11 +23,13 @@ public final class MixinPlugin implements IMixinConfigPlugin {
|
|||||||
|
|
||||||
private boolean warium;
|
private boolean warium;
|
||||||
private boolean futurism;
|
private boolean futurism;
|
||||||
|
private boolean geckolib;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoad(String mixinPackage) {
|
public void onLoad(String mixinPackage) {
|
||||||
warium = supported("valkyrien_warium", WARIUM_VERSION, "Warium VS");
|
warium = supported("valkyrien_warium", WARIUM_VERSION, "Warium VS");
|
||||||
futurism = supported("futurism", FUTURISM_VERSION, "Futurism");
|
futurism = supported("futurism", FUTURISM_VERSION, "Futurism");
|
||||||
|
geckolib = LoadingModList.get().getModFileById("geckolib") != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean supported(String modId, String version, String name) {
|
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.")) {
|
if (mixinClassName.contains(".mixin.warium.")) {
|
||||||
return 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.")) {
|
if (mixinClassName.contains(".mixin.futurism.")) {
|
||||||
return futurism;
|
return futurism;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ public final class WariumCapture {
|
|||||||
world = level;
|
world = level;
|
||||||
pos = BlockPos.containing(x, y, z);
|
pos = BlockPos.containing(x, y, z);
|
||||||
state = blockState;
|
state = blockState;
|
||||||
|
if (surfaceType.controllable && level instanceof ServerLevel serverLevel && Config.PHYSICS_RATE_AERODYNAMICS.get()) {
|
||||||
|
DeflectionSync.report(serverLevel, pos, surfaceType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<NetworkEvent.Context> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,11 @@ public final class Network {
|
|||||||
.decoder(ControlModePacket::decode)
|
.decoder(ControlModePacket::decode)
|
||||||
.consumerMainThread(ControlModePacket::handle)
|
.consumerMainThread(ControlModePacket::handle)
|
||||||
.add();
|
.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
|
// 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)
|
CHANNEL.messageBuilder(FlapPacket.class, 10, NetworkDirection.PLAY_TO_SERVER)
|
||||||
.encoder(FlapPacket::encode)
|
.encoder(FlapPacket::encode)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"compatibilityLevel": "JAVA_17",
|
"compatibilityLevel": "JAVA_17",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
"futurism.ForceInducedShipsMixin",
|
"futurism.ForceInducedShipsMixin",
|
||||||
|
"gecko.GeoModelMixin",
|
||||||
"warium.AirBrakeTickProcedureMixin",
|
"warium.AirBrakeTickProcedureMixin",
|
||||||
"warium.BypassedCompressorScriptProcedureMixin",
|
"warium.BypassedCompressorScriptProcedureMixin",
|
||||||
"warium.ControlSurfaceTickProcedureMixin",
|
"warium.ControlSurfaceTickProcedureMixin",
|
||||||
|
|||||||
Reference in new issue
Block a user