diff --git a/build.gradle b/build.gradle index faf18a7..dda01d6 100644 --- a/build.gradle +++ b/build.gradle @@ -73,8 +73,9 @@ dependencies { compileOnly files('libs/vs-core-api-1.1.0.jar', 'libs/vs-core-util-1.1.0.jar', 'libs/joml-primitives-1.10.0.jar') compileOnly 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22' - // Warium VS 1.1.6 (optional dependency, for the planned flight physics overhaul) + // Warium VS 1.1.6 and Futurism 0.0.4 (optional dependencies, the flight physics mixins only apply when they are present) compileOnly fg.deobf("blank:WariumVS:1.1.6") + compileOnly fg.deobf("blank:futurism:0.0.4") } @@ -93,6 +94,13 @@ tasks.named('processResources', ProcessResources).configure { } +// Every mixin uses remap = false (Warium and VS are not obfuscated), so no refmap is needed +tasks.named('jar', Jar).configure { + manifest { + attributes 'MixinConfigs': "${mod_id}.mixins.json" + } +} + tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } diff --git a/gradle.properties b/gradle.properties index c5442aa..3b4d505 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ mapping_version=2023.09.03-1.20.1 mod_id=improved_aerodynamics mod_name=VS: Improved Aerodynamics mod_license=MIT -mod_version=0.1.0 +mod_version=0.2.0 mod_group_id=dev.lumareth.improvedaerodynamics mod_authors=Lumareth -mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships, so they no longer slam into invisible walls at unloaded chunks. +mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships and smooths out Warium flight physics, with an optional flight model with real stalls and auto trim. diff --git a/libs/futurism-0.0.4.jar b/libs/futurism-0.0.4.jar new file mode 100644 index 0000000..698cfff Binary files /dev/null and b/libs/futurism-0.0.4.jar differ diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/Config.java b/src/main/java/dev/lumareth/improvedAerodynamics/Config.java index 333ca8f..cabcd2e 100644 --- a/src/main/java/dev/lumareth/improvedAerodynamics/Config.java +++ b/src/main/java/dev/lumareth/improvedAerodynamics/Config.java @@ -44,6 +44,56 @@ public final class Config { .comment("Ticks a placeholder chunk stays air after no ship needs it anymore, then it goes back to solid") .defineInRange("placeholderReleaseTicks", 100, 20, 6000); + static { + BUILDER.pop(); + BUILDER.comment("Warium VS (and Futurism) flight physics. Only has an effect when those mods are installed").push("flight"); + } + + public static final ForgeConfigSpec.BooleanValue SMOOTH_FORCE_DELIVERY = BUILDER + .comment("Warium computes its forces 20 times per second but applies them in only one of the physics substeps", + "in between, which makes thrust and lift jerky. When enabled, each tick's forces are spread evenly", + "over all substeps. The total push per tick stays the same, so existing craft keep their tuning") + .define("smoothForceDelivery", true); + + public static final ForgeConfigSpec.BooleanValue PHYSICS_RATE_AERODYNAMICS = BUILDER + .comment("Recompute wing, stabilizer, control surface and air brake forces every physics substep from the", + "ship's current motion instead of once per tick from its motion one tick ago. Uses Warium's own", + "formulas, so craft fly the same, minus the wobble. Required for auto trim and the improved flight model") + .define("physicsRateAerodynamics", true); + + public static final ForgeConfigSpec.BooleanValue CONTROL_SMOOTHING = BUILDER + .comment("Control surfaces move back to neutral as smoothly as they deflect, instead of snapping back on key release") + .define("controlSmoothing", true); + + public static final ForgeConfigSpec.BooleanValue IMPLICIT_DAMPING = BUILDER + .comment("Integrate aerodynamic damping implicitly, so many wings on a light craft can no longer overshoot", + "and make it shake. Only changes anything where the damping would otherwise overshoot") + .define("implicitDamping", true); + + public static final ForgeConfigSpec.BooleanValue IMPROVED_FLIGHT_MODEL = BUILDER + .comment("Replace Warium's lift and damping with a model based on each surface's angle of attack:", + "lift grows with the square of airspeed, surfaces stall one by one when their angle of attack gets", + "too high, and stalled surfaces stop damping rotation, which allows post-stall maneuvers with", + "thrust vectoring. Changes how craft fly") + .define("improvedFlightModel", false); + + public static final ForgeConfigSpec.DoubleValue DESIGN_SPEED = BUILDER + .comment("Improved flight model: airspeed (blocks per second) at which a level wing makes the same lift as in Warium.", + "Slower flight needs a higher angle of attack, faster flight a lower one") + .defineInRange("designSpeed", 40.0D, 5.0D, 300.0D); + + public static final ForgeConfigSpec.DoubleValue STALL_ANGLE_SCALE = BUILDER + .comment("Improved flight model: multiplier for the angle of attack at which surfaces stall") + .defineInRange("stallAngleScale", 1.0D, 0.5D, 2.0D); + + public static final ForgeConfigSpec.DoubleValue TRIM_AUTHORITY = BUILDER + .comment("Auto trim: largest pitch input (fraction of full deflection) the trim may hold") + .defineInRange("trimAuthority", 0.5D, 0.0D, 1.0D); + + public static final ForgeConfigSpec.DoubleValue TRIM_MIN_SPEED = BUILDER + .comment("Auto trim: below this airspeed (blocks per second) the trim holds its current value") + .defineInRange("trimMinSpeed", 10.0D, 0.0D, 100.0D); + static { BUILDER.pop(); } diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java b/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java index 1d288ea..f7dd972 100644 --- a/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java +++ b/src/main/java/dev/lumareth/improvedAerodynamics/ImprovedAerodynamics.java @@ -1,6 +1,8 @@ package dev.lumareth.improvedAerodynamics; import com.mojang.logging.LogUtils; +import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge; +import dev.lumareth.improvedAerodynamics.network.Network; import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.ModLoadingContext; @@ -16,6 +18,8 @@ public class ImprovedAerodynamics { public ImprovedAerodynamics() { MinecraftForge.EVENT_BUS.register(LookaheadLoader.class); + MinecraftForge.EVENT_BUS.register(PhysicsBridge.class); ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC); + Network.register(); } } diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/client/KeyBindings.java b/src/main/java/dev/lumareth/improvedAerodynamics/client/KeyBindings.java new file mode 100644 index 0000000..edeea51 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/client/KeyBindings.java @@ -0,0 +1,55 @@ +package dev.lumareth.improvedAerodynamics.client; + +import com.mojang.blaze3d.platform.InputConstants; +import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics; +import dev.lumareth.improvedAerodynamics.network.Network; +import dev.lumareth.improvedAerodynamics.network.ToggleAutoTrimPacket; +import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.client.settings.KeyConflictContext; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.lwjgl.glfw.GLFW; + +public final class KeyBindings { + public static final KeyMapping TOGGLE_AUTO_TRIM = new KeyMapping("key.improved_aerodynamics.toggle_auto_trim", + KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, "key.categories.improved_aerodynamics"); + + private KeyBindings() { + } + + @Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) + public static final class Registration { + private Registration() { + } + + @SubscribeEvent + public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) { + event.register(TOGGLE_AUTO_TRIM); + } + } + + @Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT) + public static final class Handler { + private Handler() { + } + + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) { + return; + } + Minecraft minecraft = Minecraft.getInstance(); + while (TOGGLE_AUTO_TRIM.consumeClick()) { + // Only while seated, and only if the server has this mod too + if (minecraft.player != null && minecraft.player.getVehicle() != null && minecraft.getConnection() != null + && Network.CHANNEL.isRemotePresent(minecraft.getConnection().getConnection())) { + Network.CHANNEL.sendToServer(new ToggleAutoTrimPacket()); + } + } + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroHolder.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroHolder.java new file mode 100644 index 0000000..46ee596 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroHolder.java @@ -0,0 +1,6 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** Added to Warium's ForceInducedShips by mixin, gives access to the ship's aerodynamics. */ +public interface AeroHolder { + AeroState improvedAerodynamics$aero(); +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroState.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroState.java new file mode 100644 index 0000000..de65dc1 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroState.java @@ -0,0 +1,309 @@ +package dev.lumareth.improvedAerodynamics.flight; + +import dev.lumareth.improvedAerodynamics.Config; +import it.unimi.dsi.fastutil.longs.Long2BooleanOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import org.joml.Matrix3dc; +import org.joml.Quaterniondc; +import org.joml.Vector3d; +import org.joml.Vector3dc; +import org.valkyrienskies.core.api.ships.PhysShip; +import org.valkyrienskies.core.api.ships.properties.ShipTransform; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Aerodynamics of one ship's Warium surfaces, evaluated every physics substep. The server thread registers the + * surfaces whose procedures ran (and so passed their conditions) during a tick, plus the control nodes they listen + * to; at the end of the tick that set is handed to the physics thread, which computes the forces from the ship's + * motion in each substep until the next tick arrives. + *

+ * The physics side also smooths pilot input, runs auto trim, and integrates damping implicitly so stiff damping + * cannot overshoot. + */ +public final class AeroState implements PhysicsBridge.Publishable { + // Warium's seat moves inputs by 0.2 per tick when pressing a key, smoothing uses that for releasing too + private static final double INPUT_RATE_PER_TICK = 0.2; + private static final double VELOCITY_EPSILON = 0.05; + private static final double ANGULAR_EPSILON = 0.005; + + private final SubstepClock clock; + + // Server thread + private Long2ObjectOpenHashMap buildingSurfaces = new Long2ObjectOpenHashMap<>(); + private Long2ObjectOpenHashMap buildingNodes = new Long2ObjectOpenHashMap<>(); + + private volatile Frame published; + + // Physics thread, except that the server thread reads trim values from nodeStates to save them + private Frame current; + private boolean[] stalled = new boolean[0]; + private boolean wingStalled; + private final Map nodeStates = new ConcurrentHashMap<>(); + private final Wrench wrench = new Wrench(); + private final Wrench probe = new Wrench(); + + private record Frame(long tick, AeroSurface[] surfaces, Long2ObjectOpenHashMap nodes) { + } + + /** Smoothed inputs and trim of a control node. */ + static final class NodeState { + double pitch; + double yaw; + double roll; + double trimIntegral; + double trimOutput; + // Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep + final Vector3d pitchResponse = new Vector3d(); + final Vector3d pitchResponseBuilding = new Vector3d(); + volatile double savedTrim; + + NodeState(NodeData data) { + pitch = data.pitch(); + yaw = data.yaw(); + roll = data.roll(); + trimIntegral = data.autoTrim() ? data.storedTrim() : 0.0; + trimOutput = trimIntegral; + savedTrim = trimIntegral; + } + } + + public AeroState(SubstepClock clock) { + this.clock = clock; + } + + // ---- Server thread ---- + + public boolean has(long surfaceKey) { + return buildingSurfaces.containsKey(surfaceKey); + } + + public boolean hasNode(long nodeKey) { + return buildingNodes.containsKey(nodeKey); + } + + public void add(AeroSurface surface) { + if (buildingSurfaces.isEmpty()) { + PhysicsBridge.touch(this); + } + buildingSurfaces.put(surface.key(), surface); + } + + public void addNode(NodeData node) { + buildingNodes.put(node.key(), node); + } + + /** Trim auto trim currently holds on a node, NaN if the physics thread has not seen the node. */ + public double trim(long nodeKey) { + NodeState state = nodeStates.get(nodeKey); + return state != null ? state.savedTrim : Double.NaN; + } + + @Override + public void publish(long tick) { + published = new Frame(tick, buildingSurfaces.values().toArray(new AeroSurface[0]), buildingNodes); + buildingSurfaces = new Long2ObjectOpenHashMap<>(); + buildingNodes = new Long2ObjectOpenHashMap<>(); + } + + // ---- Physics thread ---- + + /** Applies this substep's aerodynamic forces. Call after {@link SubstepClock#step()}. */ + public void apply(PhysShip ship, boolean autoScale) { + Frame frame = published; + if (frame == null || frame.tick < PhysicsBridge.publishedTick() || clock.stale()) { + return; + } + if (frame != current) { + switchFrame(frame); + } + + ShipTransform transform = ship.getTransform(); + Quaterniondc rotation = transform.getShipToWorldRotation(); + Vector3dc centerOfMass = transform.getPositionInShip(); + Vector3dc worldVelocity = ship.getVelocity(); + Vector3d velocity = rotation.transformInverse(worldVelocity, new Vector3d()); + Vector3d omega = rotation.transformInverse(ship.getOmega(), new Vector3d()); + double[] v = {velocity.x, velocity.y, velocity.z}; + double[] w = {omega.x, omega.y, omega.z}; + double dt = clock.substepSeconds(); + boolean improved = Config.IMPROVED_FLIGHT_MODEL.get(); + Matrix3dc inertia = ship.getMomentOfInertia(); + + updateNodes(frame, worldVelocity, rotation, inertia, w, dt, improved); + evaluate(frame, centerOfMass, v, w, improved, true, wrench); + + if (Config.IMPLICIT_DAMPING.get()) { + // Implicit Euler for the damping part: scale each axis by 1 / (1 + dt * damping / inertia), using the + // diagonal of the force's derivative with respect to velocity and angular velocity + double[] diagonal = {inertia.m00(), inertia.m11(), inertia.m22()}; + for (int axis = 0; axis < 3; axis++) { + v[axis] += VELOCITY_EPSILON; + evaluate(frame, centerOfMass, v, w, improved, false, probe); + v[axis] -= VELOCITY_EPSILON; + double damping = -(probe.force[axis] - wrench.force[axis]) / VELOCITY_EPSILON; + if (damping > 0.0) { + wrench.force[axis] /= 1.0 + dt * damping / ship.getMass(); + } + + w[axis] += ANGULAR_EPSILON; + evaluate(frame, centerOfMass, v, w, improved, false, probe); + w[axis] -= ANGULAR_EPSILON; + double angularDamping = -(probe.totalTorque(axis) - wrench.totalTorque(axis)) / ANGULAR_EPSILON; + if (angularDamping > 0.0 && diagonal[axis] > 0.0) { + double factor = 1.0 / (1.0 + dt * angularDamping / diagonal[axis]); + wrench.forceTorque[axis] *= factor; + wrench.torque[axis] *= factor; + } + } + } + + // Same frames and scaling as Warium applies its forces in + double scale = transform.getShipToWorldScaling().x(); + double forceScale = autoScale ? scale * scale * scale : 1.0; + Vector3d force = new Vector3d(wrench.force[0], wrench.force[1], wrench.force[2]).mul(forceScale); + Vector3d forceTorque = new Vector3d(wrench.forceTorque[0], wrench.forceTorque[1], wrench.forceTorque[2]).mul(forceScale); + if (force.isFinite() && forceTorque.isFinite()) { + ship.applyInvariantForce(rotation.transform(force)); + ship.applyInvariantTorque(rotation.transform(forceTorque)); + } + Vector3d torque = new Vector3d(wrench.torque[0], wrench.torque[1], wrench.torque[2]); + if (torque.lengthSquared() > 0.0 && torque.isFinite()) { + ship.applyRotDependentTorque(torque); + } + } + + private void switchFrame(Frame frame) { + // Carry stall state over by block, the surface order changes from tick to tick + Long2BooleanOpenHashMap previous = new Long2BooleanOpenHashMap(); + if (current != null) { + for (int i = 0; i < current.surfaces.length; i++) { + if (stalled[i]) { + previous.put(current.surfaces[i].key(), true); + } + } + } + stalled = new boolean[frame.surfaces.length]; + 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)); + for (NodeData node : frame.nodes.values()) { + nodeStates.computeIfAbsent(node.key(), key -> new NodeState(node)); + } + current = frame; + } + + /** + * Sums the forces of all surfaces into out. + * + * @param commit whether this is the real evaluation of the substep (updates stall state and trim response) + * rather than a probe for the damping derivatives + */ + private void evaluate(Frame frame, Vector3dc centerOfMass, double[] v, double[] w, boolean improved, boolean commit, Wrench out) { + out.clear(); + double weight = clock.weight(); + double liftConstant = ImprovedAero.liftConstant(Config.DESIGN_SPEED.get()); + double stallAngleScale = Config.STALL_ANGLE_SCALE.get(); + double[] r = new double[3]; + if (commit) { + wingStalled = false; + for (NodeState state : nodeStates.values()) { + state.pitchResponseBuilding.zero(); + } + } + + for (int i = 0; i < frame.surfaces.length; i++) { + AeroSurface s = frame.surfaces[i]; + r[0] = s.x() - centerOfMass.x(); + r[1] = s.y() - centerOfMass.y(); + r[2] = s.z() - centerOfMass.z(); + NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null; + NodeState state = node != null ? nodeStates.get(node.key()) : null; + double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0; + + if (improved) { + boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, stallAngleScale, stalled[i], out); + if (commit) { + stalled[i] = nowStalled; + wingStalled |= nowStalled && s.type() == SurfaceType.WING; + } + } else { + WariumAero.evaluate(s, v, w, r, input, weight, out); + } + + // How much a little more pitch input on this node would change the torque, for auto trim + if (commit && node != null && node.autoTrim() && s.type().controllable && s.listensToPitch()) { + double delta = 0.05; + probe.clear(); + double base = 0.0; + if (improved) { + ImprovedAero.evaluate(s, v, w, r, input, weight, liftConstant, stallAngleScale, stalled[i], probe); + } else { + WariumAero.evaluate(s, v, w, r, input, weight, probe); + } + double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)}; + probe.clear(); + double nudged = input(s, node, state, delta); + if (improved) { + ImprovedAero.evaluate(s, v, w, r, nudged, weight, liftConstant, stallAngleScale, stalled[i], probe); + } else { + WariumAero.evaluate(s, v, w, r, nudged, weight, probe); + } + state.pitchResponseBuilding.add( + (probe.totalTorque(0) - before[0]) / delta, + (probe.totalTorque(1) - before[1]) / delta, + (probe.totalTorque(2) - before[2]) / delta); + } + } + + if (commit) { + for (NodeState state : nodeStates.values()) { + state.pitchResponse.set(state.pitchResponseBuilding); + } + } + } + + /** A control surface's input, combined from its two keys and clamped like Warium does. */ + private static double input(AeroSurface s, NodeData node, NodeState state, double extraPitch) { + if (node == null || state == null) { + return 0.0; + } + double value = axisValue(s.axis1(), node, state, extraPitch) * s.multiplier1() + + axisValue(s.axis2(), node, state, extraPitch) * s.multiplier2(); + return Math.max(-1.0, Math.min(1.0, value)); + } + + private static double axisValue(ControlAxis axis, NodeData node, NodeState state, double extraPitch) { + return switch (axis) { + case PITCH -> state.pitch + state.trimOutput + extraPitch; + case YAW -> state.yaw; + case ROLL -> state.roll; + case THROTTLE -> node.throttle(); + case NONE -> 0.0; + }; + } + + private void updateNodes(Frame frame, Vector3dc worldVelocity, Quaterniondc rotation, Matrix3dc inertia, + double[] w, double dt, boolean improved) { + double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY; + for (NodeData node : frame.nodes.values()) { + NodeState state = nodeStates.get(node.key()); + if (state == null) { + continue; + } + state.pitch = moveTowards(state.pitch, node.pitch(), step); + state.yaw = moveTowards(state.yaw, node.yaw(), step); + state.roll = moveTowards(state.roll, node.roll(), step); + AutoTrim.update(state, node, worldVelocity, rotation, inertia, w, dt, improved && wingStalled); + } + } + + static double moveTowards(double value, double target, double maxStep) { + if (Math.abs(target - value) <= maxStep) { + return target; + } + return value + Math.copySign(maxStep, target - value); + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroSurface.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroSurface.java new file mode 100644 index 0000000..7ce4557 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AeroSurface.java @@ -0,0 +1,34 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** + * One Warium aerodynamic block as seen in the latest tick, everything the physics thread needs to compute its + * forces from the ship's current motion. Positions are ship coordinates. + * + * @param key BlockPos.asLong of the block + * @param x block center + * @param facingX Warium's "facing" of the block, the chord direction of the surface + * @param pressure Warium's air pressure factor at the block's altitude + * @param fluidFactor Warium's multiplier for blocks under water (1 in air) + * @param speedMultiplier Warium's aerodynamicSpeedMultiplier game rule / 100, at least 1 + * @param node BlockPos.asLong of the Vehicle Control Node the surface listens to, {@link #NO_NODE} if none + * @param brakeInput air brakes only: Warium's input value, they ignore smoothing and trim + */ +public record AeroSurface(long key, SurfaceType type, boolean slab, double x, double y, double z, int facingX, int facingZ, + double pressure, double fluidFactor, double speedMultiplier, + long node, ControlAxis axis1, double multiplier1, ControlAxis axis2, double multiplier2, + double brakeInput) { + public static final long NO_NODE = Long.MIN_VALUE; + + /** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */ + int rightX() { + return -facingZ; + } + + int rightZ() { + return facingX; + } + + boolean listensToPitch() { + return node != NO_NODE && (axis1 == ControlAxis.PITCH || axis2 == ControlAxis.PITCH); + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/AutoTrim.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AutoTrim.java new file mode 100644 index 0000000..5dd765a --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AutoTrim.java @@ -0,0 +1,74 @@ +package dev.lumareth.improvedAerodynamics.flight; + +import dev.lumareth.improvedAerodynamics.Config; +import org.joml.Matrix3dc; +import org.joml.Quaterniondc; +import org.joml.Vector3d; +import org.joml.Vector3dc; + +/** + * Auto trim for the pitch control surfaces of one control node: while the pilot gives no pitch input it adds a + * pitch offset that brings the flight path back to level and then holds it there, cancelling whatever pitching + * moment the craft has (off-center weight, thrust line, speed changes). + *

+ * Two loops: the flight path angle sets a gentle target pitch rate, and a PI controller moves the trim to reach it. + * The trim's effect is measured every substep (torque per unit of pitch input), which gives the pitch axis and lets + * the gains be expressed as angular acceleration, so the same tuning works for any craft, speed and altitude. + * The trim holds still while the pilot pitches, when too slow, and when wings stall, so it never fights maneuvers. + */ +final class AutoTrim { + // Target pitch rate per radian of flight path angle, and its limit (rad/s) + private static final double PATH_GAIN = 0.8; + private static final double MAX_PITCH_RATE = 0.1; + // Angular acceleration per rad/s of pitch rate error, and integral gain + private static final double PROPORTIONAL_GAIN = 3.0; + private static final double INTEGRAL_GAIN = 1.5; + // Fastest the trim moves, in input per second + private static final double TRIM_RATE = 1.0; + private static final double PILOT_DEADBAND = 0.01; + + private AutoTrim() { + } + + static void update(AeroState.NodeState state, NodeData node, Vector3dc worldVelocity, Quaterniondc rotation, + Matrix3dc inertia, double[] w, double dt, boolean stalled) { + if (!node.autoTrim()) { + state.trimIntegral = 0.0; + state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt); + state.savedTrim = 0.0; + return; + } + double authority = Config.TRIM_AUTHORITY.get(); + double response = state.pitchResponse.length(); + double speed = worldVelocity.length(); + boolean hold = Math.abs(node.pitch()) > PILOT_DEADBAND || speed < Config.TRIM_MIN_SPEED.get() || stalled + || response < 1.0E-6; + + double target = state.trimIntegral; + if (!hold) { + // Pitch axis in ship axes, oriented so that positive rotation is what more pitch input does + Vector3d axis = state.pitchResponse.div(response, new Vector3d()); + double axisInertia = axis.dot(inertia.transform(axis, new Vector3d())); + double pitchRate = axis.x * w[0] + axis.y * w[1] + axis.z * w[2]; + + // Does rotating about that axis raise the flight path? Sign of the vertical part of axis x direction + Vector3d worldAxis = rotation.transform(axis, new Vector3d()); + Vector3d direction = worldVelocity.normalize(new Vector3d()); + double noseUp = Math.signum(worldAxis.cross(direction, new Vector3d()).y); + double pathAngle = Math.asin(Math.max(-1.0, Math.min(1.0, direction.y))); + + double targetRate = Math.max(-MAX_PITCH_RATE, Math.min(MAX_PITCH_RATE, -PATH_GAIN * pathAngle * noseUp)); + double error = targetRate - pitchRate; + // Pitch input needed per rad/s² of angular acceleration + double gain = axisInertia / response; + state.trimIntegral = clamp(state.trimIntegral + INTEGRAL_GAIN * error * dt * gain, authority); + target = clamp(state.trimIntegral + PROPORTIONAL_GAIN * error * gain, authority); + } + state.trimOutput = AeroState.moveTowards(state.trimOutput, target, TRIM_RATE * dt); + state.savedTrim = state.trimIntegral; + } + + private static double clamp(double value, double limit) { + return Math.max(-limit, Math.min(limit, value)); + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/AutoTrimToggle.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AutoTrimToggle.java new file mode 100644 index 0000000..58da24c --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/AutoTrimToggle.java @@ -0,0 +1,63 @@ +package dev.lumareth.improvedAerodynamics.flight; + +import dev.lumareth.improvedAerodynamics.Config; +import net.mcreator.valkyrienwarium.entity.SeatEntityEntity; +import net.mcreator.valkyrienwarium.init.ValkyrienWariumModBlocks; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +/** Server side of the auto trim key. The setting lives in the control node, so it stays with the craft. */ +public final class AutoTrimToggle { + private AutoTrimToggle() { + } + + public static void toggle(ServerPlayer player) { + Entity vehicle = player.getVehicle(); + if (!(vehicle instanceof SeatEntityEntity)) { + player.displayClientMessage(Component.translatable("message.improved_aerodynamics.auto_trim.no_seat"), true); + return; + } + BlockEntity node = findNode(vehicle.level(), vehicle.blockPosition()); + if (node == null) { + player.displayClientMessage(Component.translatable("message.improved_aerodynamics.auto_trim.no_node"), true); + return; + } + + CompoundTag tag = node.getPersistentData(); + boolean enabled = !tag.getBoolean(WariumCapture.AUTO_TRIM_TAG); + tag.putBoolean(WariumCapture.AUTO_TRIM_TAG, enabled); + tag.putDouble(WariumCapture.TRIM_TAG, 0.0); + node.setChanged(); + + if (enabled && !Config.PHYSICS_RATE_AERODYNAMICS.get()) { + player.displayClientMessage(Component.translatable("message.improved_aerodynamics.auto_trim.unavailable"), true); + } else { + player.displayClientMessage(Component.translatable(enabled + ? "message.improved_aerodynamics.auto_trim.on" : "message.improved_aerodynamics.auto_trim.off"), true); + } + } + + /** + * The control node the seat under the player is linked to. Warium's control seat block finds its seat entity + * within half a block of itself, so the seat block is at the entity's block or just below it. + */ + private static BlockEntity findNode(Level level, BlockPos seatPos) { + for (BlockPos pos : new BlockPos[]{seatPos, seatPos.below()}) { + BlockEntity seat = level.getBlockEntity(pos); + if (seat == null || !seat.getPersistentData().contains("ControlX")) { + continue; + } + CompoundTag tag = seat.getPersistentData(); + BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ")); + if (level.getBlockState(nodePos).is(ValkyrienWariumModBlocks.VEHICLE_CONTROL_NODE.get())) { + return level.getBlockEntity(nodePos); + } + } + return null; + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/ControlAxis.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/ControlAxis.java new file mode 100644 index 0000000..88401f6 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/ControlAxis.java @@ -0,0 +1,28 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** The pilot input a Warium control block follows, set with its Key/Key2 (e.g. "Pitch+"). */ +public enum ControlAxis { + NONE, + PITCH, + YAW, + ROLL, + THROTTLE; + + /** Axis of a Warium key name, the +/- suffix is the multiplier, see {@link #multiplier(String)}. */ + public static ControlAxis of(String key) { + if (key.startsWith("Pitch")) { + return PITCH; + } else if (key.startsWith("Yaw")) { + return YAW; + } else if (key.startsWith("Roll")) { + return ROLL; + } else if (key.startsWith("Throttle")) { + return THROTTLE; + } + return NONE; + } + + public static double multiplier(String key) { + return key.endsWith("-") ? -1.0 : 1.0; + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/ForceSmoother.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/ForceSmoother.java new file mode 100644 index 0000000..77d4125 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/ForceSmoother.java @@ -0,0 +1,125 @@ +package dev.lumareth.improvedAerodynamics.flight; + +import net.minecraft.core.BlockPos; +import org.joml.Quaterniondc; +import org.joml.Vector3d; +import org.joml.Vector3dc; +import org.valkyrienskies.core.api.ships.PhysShip; +import org.valkyrienskies.core.api.ships.properties.ShipTransform; + +import java.util.ArrayList; +import java.util.List; + +/** + * Replacement for the force queue of Warium's (and Futurism's) ForceInducedShips. That queue is emptied by the + * first physics substep after each tick, so every force acts in one substep out of three. Here a tick's forces are + * summed on the server thread and then applied in equal parts in every substep until the next tick, which keeps the + * total push per tick identical. + *

+ * Forces are summed as a force plus a torque about the block grid origin, then moved to the center of mass when + * applied, which is exactly equivalent to applying each force at its own block. + */ +public final class ForceSmoother implements PhysicsBridge.Publishable { + private final SubstepClock clock; + private Snapshot building = new Snapshot(); + private volatile Snapshot published; + + /** One tick's forces, in the same frames and units the original code applies them in. */ + private static final class Snapshot { + long tick; + boolean empty = true; + // ForceMode.POSITION with ship directions: sum of forces and sum of (block center x force), ship coordinates + final Vector3d shipForce = new Vector3d(); + final Vector3d shipMoment = new Vector3d(); + // ForceMode.POSITION with world directions: {block center x, y, z, force x, y, z}, rare (drag handle) + final List worldForcesAtBlocks = new ArrayList<>(0); + // ForceMode.GLOBAL forces, applied at the center of mass + final Vector3d globalShipForce = new Vector3d(); + final Vector3d globalWorldForce = new Vector3d(); + // Rotations: ForceMode.POSITION ones are ship relative, ForceMode.GLOBAL ones world relative + final Vector3d shipTorque = new Vector3d(); + final Vector3d worldTorque = new Vector3d(); + } + + public ForceSmoother(SubstepClock clock) { + this.clock = clock; + } + + /** Server thread. Same arguments as ForceInducedShips.addForce, with its enums turned into flags. */ + public synchronized void addForce(BlockPos pos, Vector3dc dir, double throttle, boolean global, boolean shipDirection) { + if (throttle == 0.0) { + return; + } + Vector3d force = dir.mul(throttle, new Vector3d()); + if (global) { + (shipDirection ? building.globalShipForce : building.globalWorldForce).add(force); + } else if (shipDirection) { + Vector3d center = new Vector3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + building.shipForce.add(force); + building.shipMoment.add(center.cross(force)); + } else { + building.worldForcesAtBlocks.add(new double[]{pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, + force.x, force.y, force.z}); + } + markBuilding(); + } + + /** Server thread. Same arguments as ForceInducedShips.addRotation. */ + public synchronized void addRotation(Vector3dc rot, double throttle, boolean global) { + (global ? building.worldTorque : building.shipTorque).add(rot.mul(throttle, new Vector3d())); + markBuilding(); + } + + private void markBuilding() { + if (building.empty) { + building.empty = false; + PhysicsBridge.touch(this); + } + } + + @Override + public synchronized void publish(long tick) { + building.tick = tick; + published = building; + building = new Snapshot(); + } + + /** Physics thread, once per substep. Applies this substep's share of the latest tick's forces. */ + public void apply(PhysShip ship, boolean autoScale) { + Snapshot snapshot = published; + if (snapshot == null || snapshot.tick < PhysicsBridge.publishedTick() || clock.stale()) { + return; + } + ShipTransform transform = ship.getTransform(); + Quaterniondc rotation = transform.getShipToWorldRotation(); + Vector3dc centerOfMass = transform.getPositionInShip(); + double scale = transform.getShipToWorldScaling().x(); + double weight = clock.weight(); + // Warium's autoScale: forces grow with the cube of the ship's scale, global torques with its fifth power + double forceWeight = autoScale ? weight * scale * scale * scale : weight; + double torqueWeight = autoScale ? forceWeight * scale * scale : weight; + + if (snapshot.shipForce.lengthSquared() > 0.0 || snapshot.shipMoment.lengthSquared() > 0.0) { + // Moment about the center of mass: sum(p x F) - com x sum(F) + Vector3d moment = snapshot.shipMoment.sub(centerOfMass.cross(snapshot.shipForce, new Vector3d()), new Vector3d()); + ship.applyInvariantForce(rotation.transform(snapshot.shipForce.mul(forceWeight, new Vector3d()))); + ship.applyInvariantTorque(rotation.transform(moment.mul(forceWeight))); + } + for (double[] entry : snapshot.worldForcesAtBlocks) { + Vector3d force = new Vector3d(entry[3], entry[4], entry[5]).mul(forceWeight); + ship.applyInvariantForceToPos(force, new Vector3d(entry[0], entry[1], entry[2]).sub(centerOfMass)); + } + if (snapshot.globalShipForce.lengthSquared() > 0.0) { + ship.applyInvariantForce(rotation.transform(snapshot.globalShipForce.mul(forceWeight, new Vector3d()))); + } + if (snapshot.globalWorldForce.lengthSquared() > 0.0) { + ship.applyInvariantForce(snapshot.globalWorldForce.mul(forceWeight, new Vector3d())); + } + if (snapshot.shipTorque.lengthSquared() > 0.0) { + ship.applyRotDependentTorque(snapshot.shipTorque.mul(weight, new Vector3d())); + } + if (snapshot.worldTorque.lengthSquared() > 0.0) { + ship.applyInvariantTorque(snapshot.worldTorque.mul(torqueWeight, new Vector3d())); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/ImprovedAero.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/ImprovedAero.java new file mode 100644 index 0000000..f9498b5 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/ImprovedAero.java @@ -0,0 +1,131 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** + * Flight model based on each surface's angle of attack, used instead of {@link WariumAero} when enabled. + *

+ * Every block is treated as one unit of wing area that sees the airflow at its own position, including the part + * caused by the ship's rotation. Lift is {@code K * pressure * airspeed² * CL(angle of attack)}: slow, heavy or + * high-g flight needs a higher angle of attack for the same lift until a surface reaches its critical angle and + * stalls. Stalls are per surface, soft (lift fades over a few degrees into flat-plate behaviour) and sticky (a + * stalled surface only recovers a few degrees below its critical angle). Rotational damping is not added + * separately, it comes from the surfaces' changing angle of attack while rotating, so it vanishes for stalled + * surfaces and thrust vectoring can hold the nose past the stall. + *

+ * K is chosen so that a level wing at the configured design speed makes exactly Warium's lift. + */ +final class ImprovedAero { + // Lift slope of a finite wing per radian, about 80% of thin airfoil theory's 2 pi + private static final double LIFT_SLOPE = 2.0 * Math.PI * 0.8; + private static final double INDUCED_DRAG = 0.05; + // Fully separated flow behaves like a flat plate + private static final double SEPARATED_LIFT = 1.1; + private static final double SEPARATED_DRAG = 1.8; + private static final double STALL_BLEND = Math.toRadians(2.0); + private static final double STALL_HYSTERESIS = Math.toRadians(3.0); + private static final double MAX_DEFLECTION = Math.toRadians(20.0); + // Share of the deflection that acts as angle of attack, gives Warium's control authority at the design speed + private static final double FLAP_EFFECTIVENESS = 0.75; + // Angle of attack of a level cambered wing, where K is calibrated + private static final double REFERENCE_ANGLE = Math.toRadians(4.0); + + private ImprovedAero() { + } + + /** Lift constant K for the given design speed. */ + static double liftConstant(double designSpeed) { + return 160.0 / (designSpeed * LIFT_SLOPE * REFERENCE_ANGLE); + } + + /** + * Adds the surface's forces to out. + * + * @param v ship velocity, ship axes + * @param w ship angular velocity, ship axes + * @param r block center relative to the center of mass, ship axes + * @param input control input of the surface, -1 to 1 + * @param stalled whether the surface was stalled in the previous substep + * @return whether the surface is stalled now + */ + static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double weight, + double liftConstant, double stallAngleScale, boolean stalled, Wrench out) { + // Airflow at the block: ship velocity plus w x r + double vx = v[0] + w[1] * r[2] - w[2] * r[1]; + double vy = v[1] + w[2] * r[0] - w[0] * r[2]; + double vz = v[2] + w[0] * r[1] - w[1] * r[0]; + double localSpeed = Math.sqrt(vx * vx + vy * vy + vz * vz); + double p = s.pressure() * weight; + double m = s.speedMultiplier(); + double fd = s.fluidFactor(); + + if (s.type() == SurfaceType.AIR_BRAKE) { + double ds = -(s.facingX() * vx + s.facingZ() * vz); + double brake = -Math.abs(ds * 100.0 * s.brakeInput() * p * fd); + out.addForce(r, vx * brake, vy * brake, vz * brake); + return false; + } + + // Warium's own per-block drag + double parasitic = -localSpeed * s.type().parasiticDrag * p / m; + out.addForce(r, vx * parasitic, vy * parasitic, vz * parasitic); + + // Chord c along the facing, normal n up for horizontal surfaces and to the side for vertical ones + double cx = s.facingX(); + double cz = s.facingZ(); + double nx = s.type().vertical ? s.rightX() : 0.0; + double ny = s.type().vertical ? 0.0 : 1.0; + double nz = s.type().vertical ? s.rightZ() : 0.0; + double u = vx * cx + vz * cz; + double vn = vx * nx + vy * ny + vz * nz; + double planeSpeedSq = u * u + vn * vn; + if (planeSpeedSq < 1.0E-4) { + return stalled; + } + double planeSpeed = Math.sqrt(planeSpeedSq); + + // Moving down through the air (vn < 0) means air hits the underside: positive angle of attack. + // Surfaces work the same with air coming from either end of the chord, like in Warium + double alpha = Math.atan2(-vn, Math.abs(u)); + alpha += Math.toRadians(s.type().camber); + if (s.type().controllable) { + // Same sign convention as Warium: positive input pushes along n while moving against the facing + alpha += FLAP_EFFECTIVENESS * MAX_DEFLECTION * input * (u <= 0.0 ? 1.0 : -1.0); + } + alpha = Math.max(-Math.PI / 2.0, Math.min(Math.PI / 2.0, alpha)); + + double critical = Math.toRadians(s.slab() ? SurfaceType.SLAB_STALL_ANGLE : s.type().stallAngle) * stallAngleScale; + double absAlpha = Math.abs(alpha); + // Flips only where both blends agree, so the hysteresis never makes the force jump + if (!stalled && absAlpha > critical + STALL_BLEND) { + stalled = true; + } else if (stalled && absAlpha < critical - STALL_HYSTERESIS - STALL_BLEND) { + stalled = false; + } + double center = stalled ? critical - STALL_HYSTERESIS : critical; + double attached = 1.0 - smoothstep(center - STALL_BLEND, center + STALL_BLEND, absAlpha); + + double attachedLift = LIFT_SLOPE * alpha; + double lift = attached * attachedLift + (1.0 - attached) * SEPARATED_LIFT * Math.sin(2.0 * alpha); + double drag = attached * INDUCED_DRAG * attachedLift * attachedLift + + (1.0 - attached) * SEPARATED_DRAG * Math.sin(alpha) * Math.sin(alpha); + + double q = liftConstant * p * fd * planeSpeedSq; + // Lift is perpendicular to the airflow in the chord/normal plane, on the side of n + double side = u < 0.0 ? -1.0 : 1.0; + double lx = (Math.abs(u) * nx - side * vn * cx) / planeSpeed; + double ly = Math.abs(u) * ny / planeSpeed; + double lz = (Math.abs(u) * nz - side * vn * cz) / planeSpeed; + // Drag opposes the airflow in that plane + double dx = -(u * cx + vn * nx) / planeSpeed; + double dy = -(vn * ny) / planeSpeed; + double dz = -(u * cz + vn * nz) / planeSpeed; + double liftForce = q * lift; + double dragForce = q * drag / m; + out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce); + return stalled; + } + + private static double smoothstep(double edge0, double edge1, double x) { + double t = Math.max(0.0, Math.min(1.0, (x - edge0) / (edge1 - edge0))); + return t * t * (3.0 - 2.0 * t); + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/MixinPlugin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/MixinPlugin.java new file mode 100644 index 0000000..d5f11d4 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/MixinPlugin.java @@ -0,0 +1,79 @@ +package dev.lumareth.improvedAerodynamics.flight; + +import net.minecraftforge.fml.loading.LoadingModList; +import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; +import org.objectweb.asm.tree.ClassNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +/** + * Applies the Warium and Futurism mixins only when those mods are installed in the version they were written + * against. They hook into specific calls inside those mods, so another version is skipped with a warning rather + * than risking a crash; the rest of the mod keeps working. + */ +public final class MixinPlugin implements IMixinConfigPlugin { + private static final Logger LOGGER = LoggerFactory.getLogger("improved_aerodynamics"); + private static final String WARIUM_VERSION = "1.1.6"; + private static final String FUTURISM_VERSION = "0.0.4"; + + private boolean warium; + private boolean futurism; + + @Override + public void onLoad(String mixinPackage) { + warium = supported("valkyrien_warium", WARIUM_VERSION, "Warium VS"); + futurism = supported("futurism", FUTURISM_VERSION, "Futurism"); + } + + private static boolean supported(String modId, String version, String name) { + ModFileInfo file = LoadingModList.get().getModFileById(modId); + if (file == null) { + return false; + } + String installed = file.getMods().get(0).getVersion().toString(); + if (!installed.equals(version)) { + LOGGER.warn("{} {} is installed, but the flight physics changes are made for {} {}, they are disabled for it", + name, installed, name, version); + return false; + } + return true; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + if (mixinClassName.contains(".mixin.warium.")) { + return warium; + } + if (mixinClassName.contains(".mixin.futurism.")) { + return futurism; + } + return true; + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/NodeData.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/NodeData.java new file mode 100644 index 0000000..bc5e0a8 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/NodeData.java @@ -0,0 +1,10 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** + * Pilot inputs a Vehicle Control Node held in the latest tick (copied there from its control seat), plus its auto + * trim state. + * + * @param storedTrim trim saved in the node, used when the physics thread first sees the node + */ +public record NodeData(long key, double pitch, double yaw, double roll, double throttle, boolean autoTrim, double storedTrim) { +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/PhysicsBridge.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/PhysicsBridge.java new file mode 100644 index 0000000..422ab04 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/PhysicsBridge.java @@ -0,0 +1,63 @@ +package dev.lumareth.improvedAerodynamics.flight; + +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * Hands what the server thread collected during a tick over to the physics thread in one piece, at the end of the + * tick. Warium pushes its forces while the physics thread is already consuming them, so without this a tick's + * forces can end up split over several substeps or merged with the next tick's. + */ +public final class PhysicsBridge { + /** Something that collects data on the server thread and hands it to the physics thread at the end of each tick. */ + public interface Publishable { + void publish(long tick); + } + + private static final Set TOUCHED = Collections.newSetFromMap(new IdentityHashMap<>()); + private static long tick; + private static volatile long publishedTick; + + private PhysicsBridge() { + } + + /** Marks a collector as having data for this tick. */ + public static synchronized void touch(Publishable publishable) { + TOUCHED.add(publishable); + } + + /** + * Tick number of the latest handover. Data published at an older tick is outdated: its ship received nothing in + * the latest tick, so nothing should be applied anymore. + */ + public static long publishedTick() { + return publishedTick; + } + + @SubscribeEvent + public static void onServerTick(TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.END) { + return; + } + synchronized (PhysicsBridge.class) { + tick++; + for (Publishable publishable : TOUCHED) { + publishable.publish(tick); + } + TOUCHED.clear(); + publishedTick = tick; + } + } + + @SubscribeEvent + public static void onServerStopped(ServerStoppedEvent event) { + synchronized (PhysicsBridge.class) { + TOUCHED.clear(); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/SubstepClock.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/SubstepClock.java new file mode 100644 index 0000000..060576d --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/SubstepClock.java @@ -0,0 +1,44 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** + * Tracks how many physics substeps run per game tick for one ship (VS's physicsTicksPerGameTick, 3 by default), + * measured rather than read from VS's config so it also holds while the server or the physics thread lags. + * Only used on the physics thread. + */ +public final class SubstepClock { + private static final double TICK_SECONDS = 0.05; + // Keep applying the last tick's data for this many ticks while the server lags, then stop + private static final int MAX_HOLD_TICKS = 10; + + private long lastTick = -1; + private int substepsSinceTick; + private double substepsPerTick = 3.0; + + /** Call once at the start of every physics substep. */ + public void step() { + long tick = PhysicsBridge.publishedTick(); + if (tick != lastTick) { + if (lastTick >= 0 && substepsSinceTick > 0 && substepsSinceTick <= 20) { + substepsPerTick += (substepsSinceTick - substepsPerTick) * 0.1; + } + lastTick = tick; + substepsSinceTick = 0; + } + substepsSinceTick++; + } + + /** Share of a tick's force that one substep applies, so the total per tick stays what Warium intended. */ + public double weight() { + return 1.0 / substepsPerTick; + } + + /** Length of one substep in seconds. */ + public double substepSeconds() { + return TICK_SECONDS / substepsPerTick; + } + + /** The server has not delivered a tick for so long that its last forces should no longer be applied. */ + public boolean stale() { + return substepsSinceTick > MAX_HOLD_TICKS * substepsPerTick; + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/SurfaceType.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/SurfaceType.java new file mode 100644 index 0000000..b38e242 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/SurfaceType.java @@ -0,0 +1,35 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** + * Kinds of Warium aerodynamic blocks, one per Warium procedure. The numbers are only used by the improved flight + * model: the angle of attack (degrees) at which the surface stalls, how far its airfoil's lift is shifted towards + * positive angles (Warium calls wings asymmetric aerofoils, stabilizers symmetric ones) and its parasitic drag + * coefficient, which is Warium's own per-block drag. + */ +public enum SurfaceType { + WING(false, false, 16.0, 4.0, 1.0), + STABILIZER(false, false, 14.0, 0.0, 1.0), + VERTICAL_WING(true, false, 15.0, 0.0, 0.5), + CONTROL_SURFACE(false, true, 15.0, 0.0, 0.0), + VERTICAL_CONTROL_SURFACE(true, true, 15.0, 0.0, 0.0), + AIR_BRAKE(false, false, 0.0, 0.0, 0.0); + + // Slab variants are thinner and stall earlier + static final double SLAB_STALL_ANGLE = 12.0; + + /** Surface stands upright, so its lift acts sideways. */ + final boolean vertical; + /** Deflects with pilot input. */ + final boolean controllable; + final double stallAngle; + final double camber; + final double parasiticDrag; + + SurfaceType(boolean vertical, boolean controllable, double stallAngle, double camber, double parasiticDrag) { + this.vertical = vertical; + this.controllable = controllable; + this.stallAngle = stallAngle; + this.camber = camber; + this.parasiticDrag = parasiticDrag; + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumAero.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumAero.java new file mode 100644 index 0000000..312dbf0 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumAero.java @@ -0,0 +1,78 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** + * Warium VS 1.1.6's aerodynamic forces, term for term as its block procedures compute them, but evaluated from the + * ship's current motion. Like in Warium every block uses the velocity of the ship's center of mass. The procedures' + * conditions (air around the block, minimum speed, linked control node) were already checked on the server thread. + * Quirks are kept on purpose so craft fly exactly as before: vertical wings have no drag (Warium never sets the + * pressure that term uses), vertical control surface damping and air brakes ignore the speed multiplier. + */ +final class WariumAero { + private WariumAero() { + } + + /** + * @param v ship velocity, ship axes + * @param w ship angular velocity, ship axes + * @param r block center relative to the center of mass, ship axes + * @param input control input of the surface, already combined from its keys and clamped like Warium does + * @param weight share of a tick's force this substep applies + */ + static void evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double weight, Wrench out) { + double speed = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + double p = s.pressure() * weight; + double m = s.speedMultiplier(); + double fd = s.fluidFactor(); + int fx = s.facingX(); + int fz = s.facingZ(); + // Speed along the facing, Warium's "directionalspeed" of control surfaces and air brakes + double ds = -(fx * v[0] + fz * v[2]); + + switch (s.type()) { + case WING -> { + out.addForce(r, 0.0, v[1] * Math.abs(v[1]) * -45.0 * p / m, 0.0); + out.addForce(r, 0.0, Math.abs(fx * v[0] + fz * v[2]) * 160.0 * p, 0.0); + double damping = speed * 600.0 * p / m; + out.addTorque(-w[0] * damping, -0.1 * w[1] * damping, -w[2] * damping); + double drag = -speed * p / m; + out.addForce(r, v[0] * drag, v[1] * drag, v[2] * drag); + } + case STABILIZER -> { + out.addForce(r, 0.0, v[1] * Math.abs(v[1]) * -45.0 * p / m, 0.0); + double damping = speed * 600.0 * p / m; + out.addTorque(-w[0] * damping, 0.0, -w[2] * damping); + double drag = -speed * p / m; + out.addForce(r, v[0] * drag, v[1] * drag, v[2] * drag); + } + case VERTICAL_WING -> { + sideForce(s, v, r, p, m, fd, out); + double damping = speed * 900.0 * p / m; + out.addTorque(-0.1 * w[0] * damping, -w[1] * damping, -0.1 * w[2] * damping); + } + case CONTROL_SURFACE -> { + out.addForce(r, 0.0, ds * 600.0 * input * p * fd / m, 0.0); + double damping = Math.abs(v[1]) * -90.0 * p / m; + out.addForce(r, 0.1 * v[0] * damping, v[1] * damping, 0.1 * v[2] * damping); + } + case VERTICAL_CONTROL_SURFACE -> { + double control = ds * 600.0 * input * p * fd / m; + out.addForce(r, s.rightX() * control, 0.0, s.rightZ() * control); + sideForce(s, v, r, p, m, fd, out); + double damping = speed * 600.0 * p; + out.addTorque(-0.1 * w[0] * damping, -w[1] * damping, -0.1 * w[2] * damping); + } + case AIR_BRAKE -> { + double brake = -Math.abs(ds * 100.0 * s.brakeInput() * p * fd); + out.addForce(r, v[0] * brake, v[1] * brake, v[2] * brake); + } + } + } + + /** Vertical wings resisting sideways motion through them. */ + private static void sideForce(AeroSurface s, double[] v, double[] r, double p, double m, double fd, Wrench out) { + int ax = Math.abs(s.rightX()); + int az = Math.abs(s.rightZ()); + double throttle = fd * (ax * (Math.abs(v[0]) + 0.1) + az * (Math.abs(v[2]) + 0.1)) * -40.0 * p / m; + out.addForce(r, v[0] * ax * throttle, 0.0, v[2] * az * throttle); + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumCapture.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumCapture.java new file mode 100644 index 0000000..da487f9 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/WariumCapture.java @@ -0,0 +1,167 @@ +package dev.lumareth.improvedAerodynamics.flight; + +import dev.lumareth.improvedAerodynamics.Config; +import net.mcreator.valkyrienwarium.init.ValkyrienWariumModBlocks; +import net.mcreator.valkyrienwarium.init.ValkyrienWariumModGameRules; +import net.mcreator.valkyrienwarium.procedures.PressureGetProcedure; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraftforge.registries.ForgeRegistries; +import org.joml.Vector3d; +import org.valkyrienskies.core.api.ships.Ship; +import org.valkyrienskies.mod.common.VSGameUtilsKt; + +/** + * Server thread side of the physics rate aerodynamics: the Warium procedure mixins call {@link #begin} when a + * procedure starts and {@link #capture} instead of each force it adds. A procedure only adds forces after passing + * all its conditions (air around the block, minimum speed, linked node), so a captured block is one whose forces + * apply this tick; its forces are then computed on the physics thread from the ship's current motion. + */ +public final class WariumCapture { + public static final String AUTO_TRIM_TAG = "improved_aerodynamics_autotrim"; + public static final String TRIM_TAG = "improved_aerodynamics_trim"; + private static final int TRIM_SAVE_INTERVAL = 20; + + // The procedure currently running, procedures run one at a time on the server thread + private static SurfaceType type; + private static LevelAccessor world; + private static BlockPos pos; + private static BlockState state; + + private WariumCapture() { + } + + public static void begin(SurfaceType surfaceType, LevelAccessor level, double x, double y, double z, BlockState blockState) { + type = surfaceType; + world = level; + pos = BlockPos.containing(x, y, z); + state = blockState; + } + + /** + * Called instead of a force the running procedure adds. + * + * @return true if the block was taken over, so the original force must not be added + */ + public static boolean capture(ForceInducedShips inducer) { + if (!Config.PHYSICS_RATE_AERODYNAMICS.get() || !(world instanceof ServerLevel level)) { + return false; + } + // ForceInducedShips is final, so the compiler needs the detour through Object to accept the mixed-in interface + AeroState aero = ((AeroHolder) (Object) inducer).improvedAerodynamics$aero(); + if (!aero.has(pos.asLong())) { + aero.add(createSurface(level, aero)); + } + return true; + } + + private static AeroSurface createSurface(ServerLevel level, AeroState aero) { + Direction facing = facing(state); + ResourceLocation id = ForgeRegistries.BLOCKS.getKey(state.getBlock()); + boolean slab = id != null && id.getPath().contains("slab"); + double speedMultiplier = Math.max(1, level.getGameRules().getInt(ValkyrienWariumModGameRules.AERODYNAMIC_SPEED_MULTIPLIER) / 100); + double pressure = PressureGetProcedure.execute(level, pos.getX(), pos.getY(), pos.getZ()); + + long node = AeroSurface.NO_NODE; + String key = "Null"; + String key2 = "Null"; + double brakeInput = 0.0; + if (type.controllable || type == SurfaceType.AIR_BRAKE) { + BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity != null) { + CompoundTag tag = blockEntity.getPersistentData(); + key = tag.getString("Key"); + key2 = type == SurfaceType.AIR_BRAKE ? "Null" : tag.getString("Key2"); + BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ")); + if (level.getBlockState(nodePos).is(ValkyrienWariumModBlocks.VEHICLE_CONTROL_NODE.get())) { + node = nodePos.asLong(); + readNode(level, nodePos, aero); + if (type == SurfaceType.AIR_BRAKE) { + brakeInput = nodeValue(level, nodePos, key) * ControlAxis.multiplier(key); + } + } + } + } + + return new AeroSurface(pos.asLong(), type, slab, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, + facing.getStepX(), facing.getStepZ(), pressure, fluidFactor(level), speedMultiplier, + node, ControlAxis.of(key), ControlAxis.multiplier(key), ControlAxis.of(key2), ControlAxis.multiplier(key2), + brakeInput); + } + + /** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */ + private static void readNode(ServerLevel level, BlockPos nodePos, AeroState aero) { + long nodeKey = nodePos.asLong(); + BlockEntity blockEntity = level.getBlockEntity(nodePos); + if (aero.hasNode(nodeKey) || blockEntity == null) { + return; + } + CompoundTag tag = blockEntity.getPersistentData(); + boolean autoTrim = tag.getBoolean(AUTO_TRIM_TAG); + double storedTrim = tag.getDouble(TRIM_TAG); + if (autoTrim && level.getGameTime() % TRIM_SAVE_INTERVAL == 0) { + double trim = aero.trim(nodeKey); + if (!Double.isNaN(trim) && Math.abs(trim - storedTrim) > 1.0E-3) { + tag.putDouble(TRIM_TAG, trim); + blockEntity.setChanged(); + storedTrim = trim; + } + } + aero.addNode(new NodeData(nodeKey, tag.getDouble("Pitch"), tag.getDouble("Yaw"), tag.getDouble("Roll"), + tag.getDouble("Throttle"), autoTrim, storedTrim)); + } + + /** Raw node value for a key like "Pitch+", as Warium's air brakes read it. */ + private static double nodeValue(ServerLevel level, BlockPos nodePos, String key) { + BlockEntity blockEntity = level.getBlockEntity(nodePos); + if (blockEntity == null || key.isEmpty()) { + return 0.0; + } + String name = key.endsWith("+") || key.endsWith("-") ? key.substring(0, key.length() - 1) : key; + return blockEntity.getPersistentData().getDouble(name); + } + + /** Warium's multiplier for surfaces under water, checked at the block's world position. */ + private static double fluidFactor(ServerLevel level) { + double underWater = switch (type) { + case VERTICAL_WING -> 15.0; + case CONTROL_SURFACE, VERTICAL_CONTROL_SURFACE -> 80.0; + case AIR_BRAKE -> 40.0; + default -> 1.0; + }; + if (underWater == 1.0) { + return 1.0; + } + Vector3d center = new Vector3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); + Ship ship = VSGameUtilsKt.getShipManagingPos(level, pos); + if (ship != null) { + ship.getShipToWorld().transformPosition(center); + } + boolean inLiquid = level.getBlockState(BlockPos.containing(center.x, center.y, center.z)).getBlock() instanceof LiquidBlock; + return inLiquid ? underWater : 1.0; + } + + /** Warium's way of reading a block's direction: its "facing", else the positive end of its "axis", else north. */ + private static Direction facing(BlockState blockState) { + Property facing = blockState.getBlock().getStateDefinition().getProperty("facing"); + if (facing instanceof DirectionProperty directionProperty) { + return blockState.getValue(directionProperty); + } + Property axis = blockState.getBlock().getStateDefinition().getProperty("axis"); + if (axis instanceof EnumProperty enumProperty && blockState.getValue(enumProperty) instanceof Direction.Axis value) { + return Direction.fromAxisAndDirection(value, Direction.AxisDirection.POSITIVE); + } + return Direction.NORTH; + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/Wrench.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/Wrench.java new file mode 100644 index 0000000..ad1082b --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/Wrench.java @@ -0,0 +1,39 @@ +package dev.lumareth.improvedAerodynamics.flight; + +/** + * Sum of the forces and torques of a set of surfaces, in ship axes. Torques of forces are taken about the center of + * mass; pure torques (Warium's ship relative rotations) are kept apart since VS scales them differently. + */ +final class Wrench { + final double[] force = new double[3]; + final double[] forceTorque = new double[3]; + final double[] torque = new double[3]; + + void clear() { + for (int i = 0; i < 3; i++) { + force[i] = 0.0; + forceTorque[i] = 0.0; + torque[i] = 0.0; + } + } + + /** Force on the surface's block, with r the block center relative to the center of mass. */ + void addForce(double[] r, double x, double y, double z) { + force[0] += x; + force[1] += y; + force[2] += z; + forceTorque[0] += r[1] * z - r[2] * y; + forceTorque[1] += r[2] * x - r[0] * z; + forceTorque[2] += r[0] * y - r[1] * x; + } + + void addTorque(double x, double y, double z) { + torque[0] += x; + torque[1] += y; + torque[2] += z; + } + + double totalTorque(int axis) { + return forceTorque[axis] + torque[axis]; + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/futurism/ForceInducedShipsMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/futurism/ForceInducedShipsMixin.java new file mode 100644 index 0000000..82b1034 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/futurism/ForceInducedShipsMixin.java @@ -0,0 +1,84 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.futurism; + +import com.mojang.datafixers.util.Pair; +import dev.lumareth.improvedAerodynamics.Config; +import dev.lumareth.improvedAerodynamics.flight.ForceSmoother; +import dev.lumareth.improvedAerodynamics.flight.SubstepClock; +import net.mcreator.futurism.ships.ForceData; +import net.mcreator.futurism.ships.ForceDirectionMode; +import net.mcreator.futurism.ships.ForceInducedShips; +import net.mcreator.futurism.ships.ForceMode; +import net.mcreator.futurism.ships.RotData; +import net.minecraft.core.BlockPos; +import org.joml.Vector3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +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.callback.CallbackInfo; +import org.valkyrienskies.core.api.ships.PhysShip; +import org.valkyrienskies.core.api.world.PhysLevel; + +import java.util.Stack; + +/** Futurism ships its own copy of Warium's ForceInducedShips (vectored afterburners and other thrusters), same treatment. */ +@Mixin(value = ForceInducedShips.class, remap = false) +public abstract class ForceInducedShipsMixin { + @Shadow + public boolean autoScale; + @Shadow + public Stack> nextTickForces; + @Shadow + public Stack nextTickRotations; + + @Unique + private final transient SubstepClock improvedAerodynamics$clock = new SubstepClock(); + @Unique + private final transient ForceSmoother improvedAerodynamics$smoother = new ForceSmoother(improvedAerodynamics$clock); + + @Inject(method = "addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/futurism/ships/ForceData;)V", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addForce(BlockPos pos, ForceData data, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addForce(pos, data.dir, data.throttle, data.mode == ForceMode.GLOBAL, + data.dirMode == ForceDirectionMode.SHIP); + ci.cancel(); + } + } + + @Inject(method = "addForce(Lnet/minecraft/core/BlockPos;Lorg/joml/Vector3d;DLnet/mcreator/futurism/ships/ForceMode;Lnet/mcreator/futurism/ships/ForceDirectionMode;)V", + at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addForce(BlockPos pos, Vector3d dir, double throttle, ForceMode mode, ForceDirectionMode dirMode, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addForce(pos, dir, throttle, mode == ForceMode.GLOBAL, dirMode == ForceDirectionMode.SHIP); + ci.cancel(); + } + } + + @Inject(method = "addRotation(Lnet/mcreator/futurism/ships/RotData;)V", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addRotation(RotData data, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addRotation(data.rot, data.throttle, data.mode == ForceMode.GLOBAL); + ci.cancel(); + } + } + + @Inject(method = "addRotation(Lorg/joml/Vector3d;DLnet/mcreator/futurism/ships/ForceMode;)V", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addRotation(Vector3d rot, double throttle, ForceMode mode, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addRotation(rot, throttle, mode == ForceMode.GLOBAL); + ci.cancel(); + } + } + + @Inject(method = "physTick", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$physTick(PhysShip ship, PhysLevel level, CallbackInfo ci) { + improvedAerodynamics$clock.step(); + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + nextTickForces.clear(); + nextTickRotations.clear(); + improvedAerodynamics$smoother.apply(ship, autoScale); + ci.cancel(); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/AirBrakeTickProcedureMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/AirBrakeTickProcedureMixin.java new file mode 100644 index 0000000..c491a6a --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/AirBrakeTickProcedureMixin.java @@ -0,0 +1,32 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import dev.lumareth.improvedAerodynamics.flight.SurfaceType; +import dev.lumareth.improvedAerodynamics.flight.WariumCapture; +import net.mcreator.valkyrienwarium.procedures.AirBrakeTickProcedure; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Hands this procedure's forces to the physics rate aerodynamics, see {@link WariumCapture}. */ +@Mixin(value = AirBrakeTickProcedure.class, remap = false) +public abstract class AirBrakeTickProcedureMixin { + @Inject(method = "execute", at = @At("HEAD")) + private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) { + WariumCapture.begin(SurfaceType.AIR_BRAKE, world, x, y, z, blockstate); + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V")) + private static void improvedAerodynamics$addForce(ForceInducedShips inducer, BlockPos pos, ForceData data) { + if (!WariumCapture.capture(inducer)) { + inducer.addForce(pos, data); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/ControlSurfaceTickProcedureMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/ControlSurfaceTickProcedureMixin.java new file mode 100644 index 0000000..37f953d --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/ControlSurfaceTickProcedureMixin.java @@ -0,0 +1,32 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import dev.lumareth.improvedAerodynamics.flight.SurfaceType; +import dev.lumareth.improvedAerodynamics.flight.WariumCapture; +import net.mcreator.valkyrienwarium.procedures.ControlSurfaceTickProcedure; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Hands this procedure's forces to the physics rate aerodynamics, see {@link WariumCapture}. */ +@Mixin(value = ControlSurfaceTickProcedure.class, remap = false) +public abstract class ControlSurfaceTickProcedureMixin { + @Inject(method = "execute", at = @At("HEAD")) + private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) { + WariumCapture.begin(SurfaceType.CONTROL_SURFACE, world, x, y, z, blockstate); + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V")) + private static void improvedAerodynamics$addForce(ForceInducedShips inducer, BlockPos pos, ForceData data) { + if (!WariumCapture.capture(inducer)) { + inducer.addForce(pos, data); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/ForceInducedShipsMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/ForceInducedShipsMixin.java new file mode 100644 index 0000000..8005a52 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/ForceInducedShipsMixin.java @@ -0,0 +1,100 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import com.mojang.datafixers.util.Pair; +import dev.lumareth.improvedAerodynamics.Config; +import dev.lumareth.improvedAerodynamics.flight.AeroHolder; +import dev.lumareth.improvedAerodynamics.flight.AeroState; +import dev.lumareth.improvedAerodynamics.flight.ForceSmoother; +import dev.lumareth.improvedAerodynamics.flight.SubstepClock; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceDirectionMode; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.mcreator.valkyrienwarium.ships.ForceMode; +import net.mcreator.valkyrienwarium.ships.RotData; +import net.minecraft.core.BlockPos; +import org.joml.Vector3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +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.callback.CallbackInfo; +import org.valkyrienskies.core.api.ships.PhysShip; +import org.valkyrienskies.core.api.world.PhysLevel; + +import java.util.Stack; + +/** + * Routes Warium's force queue through {@link ForceSmoother} and evaluates the ship's aerodynamics every substep. + * The added fields are transient so VS does not save them with the attachment. + */ +@Mixin(value = ForceInducedShips.class, remap = false) +public abstract class ForceInducedShipsMixin implements AeroHolder { + @Shadow + public boolean autoScale; + @Shadow + public Stack> nextTickForces; + @Shadow + public Stack nextTickRotations; + + @Unique + private final transient SubstepClock improvedAerodynamics$clock = new SubstepClock(); + @Unique + private final transient ForceSmoother improvedAerodynamics$smoother = new ForceSmoother(improvedAerodynamics$clock); + @Unique + private final transient AeroState improvedAerodynamics$aero = new AeroState(improvedAerodynamics$clock); + + @Override + public AeroState improvedAerodynamics$aero() { + return improvedAerodynamics$aero; + } + + @Inject(method = "addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addForce(BlockPos pos, ForceData data, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addForce(pos, data.dir, data.throttle, data.mode == ForceMode.GLOBAL, + data.dirMode == ForceDirectionMode.SHIP); + ci.cancel(); + } + } + + @Inject(method = "addForce(Lnet/minecraft/core/BlockPos;Lorg/joml/Vector3d;DLnet/mcreator/valkyrienwarium/ships/ForceMode;Lnet/mcreator/valkyrienwarium/ships/ForceDirectionMode;)V", + at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addForce(BlockPos pos, Vector3d dir, double throttle, ForceMode mode, ForceDirectionMode dirMode, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addForce(pos, dir, throttle, mode == ForceMode.GLOBAL, dirMode == ForceDirectionMode.SHIP); + ci.cancel(); + } + } + + @Inject(method = "addRotation(Lnet/mcreator/valkyrienwarium/ships/RotData;)V", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addRotation(RotData data, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addRotation(data.rot, data.throttle, data.mode == ForceMode.GLOBAL); + ci.cancel(); + } + } + + @Inject(method = "addRotation(Lorg/joml/Vector3d;DLnet/mcreator/valkyrienwarium/ships/ForceMode;)V", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$addRotation(Vector3d rot, double throttle, ForceMode mode, CallbackInfo ci) { + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + improvedAerodynamics$smoother.addRotation(rot, throttle, mode == ForceMode.GLOBAL); + ci.cancel(); + } + } + + @Inject(method = "physTick", at = @At("HEAD"), cancellable = true) + private void improvedAerodynamics$physTick(PhysShip ship, PhysLevel level, CallbackInfo ci) { + improvedAerodynamics$clock.step(); + if (Config.PHYSICS_RATE_AERODYNAMICS.get()) { + improvedAerodynamics$aero.apply(ship, autoScale); + } + if (Config.SMOOTH_FORCE_DELIVERY.get()) { + // Anything queued before smoothing was switched on would otherwise stay in the stacks forever + nextTickForces.clear(); + nextTickRotations.clear(); + improvedAerodynamics$smoother.apply(ship, autoScale); + ci.cancel(); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/HorizontalStabilizerScriptProcedureMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/HorizontalStabilizerScriptProcedureMixin.java new file mode 100644 index 0000000..1b909eb --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/HorizontalStabilizerScriptProcedureMixin.java @@ -0,0 +1,42 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import dev.lumareth.improvedAerodynamics.flight.SurfaceType; +import dev.lumareth.improvedAerodynamics.flight.WariumCapture; +import net.mcreator.valkyrienwarium.procedures.HorizontalStabilizerScriptProcedure; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.mcreator.valkyrienwarium.ships.ForceMode; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.joml.Vector3d; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Hands this procedure's forces to the physics rate aerodynamics, see {@link WariumCapture}. */ +@Mixin(value = HorizontalStabilizerScriptProcedure.class, remap = false) +public abstract class HorizontalStabilizerScriptProcedureMixin { + @Inject(method = "execute", at = @At("HEAD")) + private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) { + WariumCapture.begin(SurfaceType.STABILIZER, world, x, y, z, blockstate); + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V")) + private static void improvedAerodynamics$addForce(ForceInducedShips inducer, BlockPos pos, ForceData data) { + if (!WariumCapture.capture(inducer)) { + inducer.addForce(pos, data); + } + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addRotation(Lorg/joml/Vector3d;DLnet/mcreator/valkyrienwarium/ships/ForceMode;)V")) + private static void improvedAerodynamics$addRotation(ForceInducedShips inducer, Vector3d rot, double throttle, ForceMode mode) { + if (!WariumCapture.capture(inducer)) { + inducer.addRotation(rot, throttle, mode); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/HorizontalWingScriptProcedureMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/HorizontalWingScriptProcedureMixin.java new file mode 100644 index 0000000..b3875d9 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/HorizontalWingScriptProcedureMixin.java @@ -0,0 +1,42 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import dev.lumareth.improvedAerodynamics.flight.SurfaceType; +import dev.lumareth.improvedAerodynamics.flight.WariumCapture; +import net.mcreator.valkyrienwarium.procedures.HorizontalWingScriptProcedure; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.mcreator.valkyrienwarium.ships.ForceMode; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.joml.Vector3d; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Hands this procedure's forces to the physics rate aerodynamics, see {@link WariumCapture}. */ +@Mixin(value = HorizontalWingScriptProcedure.class, remap = false) +public abstract class HorizontalWingScriptProcedureMixin { + @Inject(method = "execute", at = @At("HEAD")) + private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) { + WariumCapture.begin(SurfaceType.WING, world, x, y, z, blockstate); + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V")) + private static void improvedAerodynamics$addForce(ForceInducedShips inducer, BlockPos pos, ForceData data) { + if (!WariumCapture.capture(inducer)) { + inducer.addForce(pos, data); + } + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addRotation(Lorg/joml/Vector3d;DLnet/mcreator/valkyrienwarium/ships/ForceMode;)V")) + private static void improvedAerodynamics$addRotation(ForceInducedShips inducer, Vector3d rot, double throttle, ForceMode mode) { + if (!WariumCapture.capture(inducer)) { + inducer.addRotation(rot, throttle, mode); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalAirBrakeTickProcedureMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalAirBrakeTickProcedureMixin.java new file mode 100644 index 0000000..4e77563 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalAirBrakeTickProcedureMixin.java @@ -0,0 +1,32 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import dev.lumareth.improvedAerodynamics.flight.SurfaceType; +import dev.lumareth.improvedAerodynamics.flight.WariumCapture; +import net.mcreator.valkyrienwarium.procedures.VerticalAirBrakeTickProcedure; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Hands this procedure's forces to the physics rate aerodynamics, see {@link WariumCapture}. */ +@Mixin(value = VerticalAirBrakeTickProcedure.class, remap = false) +public abstract class VerticalAirBrakeTickProcedureMixin { + @Inject(method = "execute", at = @At("HEAD")) + private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) { + WariumCapture.begin(SurfaceType.AIR_BRAKE, world, x, y, z, blockstate); + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V")) + private static void improvedAerodynamics$addForce(ForceInducedShips inducer, BlockPos pos, ForceData data) { + if (!WariumCapture.capture(inducer)) { + inducer.addForce(pos, data); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalControlSurfaceTickProcedureMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalControlSurfaceTickProcedureMixin.java new file mode 100644 index 0000000..6b8d755 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalControlSurfaceTickProcedureMixin.java @@ -0,0 +1,42 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import dev.lumareth.improvedAerodynamics.flight.SurfaceType; +import dev.lumareth.improvedAerodynamics.flight.WariumCapture; +import net.mcreator.valkyrienwarium.procedures.VerticalControlSurfaceTickProcedure; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.mcreator.valkyrienwarium.ships.ForceMode; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.joml.Vector3d; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Hands this procedure's forces to the physics rate aerodynamics, see {@link WariumCapture}. */ +@Mixin(value = VerticalControlSurfaceTickProcedure.class, remap = false) +public abstract class VerticalControlSurfaceTickProcedureMixin { + @Inject(method = "execute", at = @At("HEAD")) + private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) { + WariumCapture.begin(SurfaceType.VERTICAL_CONTROL_SURFACE, world, x, y, z, blockstate); + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V")) + private static void improvedAerodynamics$addForce(ForceInducedShips inducer, BlockPos pos, ForceData data) { + if (!WariumCapture.capture(inducer)) { + inducer.addForce(pos, data); + } + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addRotation(Lorg/joml/Vector3d;DLnet/mcreator/valkyrienwarium/ships/ForceMode;)V")) + private static void improvedAerodynamics$addRotation(ForceInducedShips inducer, Vector3d rot, double throttle, ForceMode mode) { + if (!WariumCapture.capture(inducer)) { + inducer.addRotation(rot, throttle, mode); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalWingScriptProcedureMixin.java b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalWingScriptProcedureMixin.java new file mode 100644 index 0000000..5da5c68 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/flight/mixin/warium/VerticalWingScriptProcedureMixin.java @@ -0,0 +1,42 @@ +package dev.lumareth.improvedAerodynamics.flight.mixin.warium; + +import dev.lumareth.improvedAerodynamics.flight.SurfaceType; +import dev.lumareth.improvedAerodynamics.flight.WariumCapture; +import net.mcreator.valkyrienwarium.procedures.VerticalWingScriptProcedure; +import net.mcreator.valkyrienwarium.ships.ForceData; +import net.mcreator.valkyrienwarium.ships.ForceInducedShips; +import net.mcreator.valkyrienwarium.ships.ForceMode; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.joml.Vector3d; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Hands this procedure's forces to the physics rate aerodynamics, see {@link WariumCapture}. */ +@Mixin(value = VerticalWingScriptProcedure.class, remap = false) +public abstract class VerticalWingScriptProcedureMixin { + @Inject(method = "execute", at = @At("HEAD")) + private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) { + WariumCapture.begin(SurfaceType.VERTICAL_WING, world, x, y, z, blockstate); + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addForce(Lnet/minecraft/core/BlockPos;Lnet/mcreator/valkyrienwarium/ships/ForceData;)V")) + private static void improvedAerodynamics$addForce(ForceInducedShips inducer, BlockPos pos, ForceData data) { + if (!WariumCapture.capture(inducer)) { + inducer.addForce(pos, data); + } + } + + @Redirect(method = "execute", at = @At(value = "INVOKE", + target = "Lnet/mcreator/valkyrienwarium/ships/ForceInducedShips;addRotation(Lorg/joml/Vector3d;DLnet/mcreator/valkyrienwarium/ships/ForceMode;)V")) + private static void improvedAerodynamics$addRotation(ForceInducedShips inducer, Vector3d rot, double throttle, ForceMode mode) { + if (!WariumCapture.capture(inducer)) { + inducer.addRotation(rot, throttle, mode); + } + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/network/Network.java b/src/main/java/dev/lumareth/improvedAerodynamics/network/Network.java new file mode 100644 index 0000000..5249949 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/network/Network.java @@ -0,0 +1,27 @@ +package dev.lumareth.improvedAerodynamics.network; + +import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.simple.SimpleChannel; + +/** Optional on both sides: players without the mod can still join a server with it, and the other way round. */ +public final class Network { + private static final String VERSION = "1"; + + public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel( + new ResourceLocation(ImprovedAerodynamics.MOD_ID, "main"), () -> VERSION, + NetworkRegistry.acceptMissingOr(VERSION), NetworkRegistry.acceptMissingOr(VERSION)); + + private Network() { + } + + public static void register() { + CHANNEL.messageBuilder(ToggleAutoTrimPacket.class, 0, NetworkDirection.PLAY_TO_SERVER) + .encoder(ToggleAutoTrimPacket::encode) + .decoder(ToggleAutoTrimPacket::decode) + .consumerMainThread(ToggleAutoTrimPacket::handle) + .add(); + } +} diff --git a/src/main/java/dev/lumareth/improvedAerodynamics/network/ToggleAutoTrimPacket.java b/src/main/java/dev/lumareth/improvedAerodynamics/network/ToggleAutoTrimPacket.java new file mode 100644 index 0000000..5f5b484 --- /dev/null +++ b/src/main/java/dev/lumareth/improvedAerodynamics/network/ToggleAutoTrimPacket.java @@ -0,0 +1,26 @@ +package dev.lumareth.improvedAerodynamics.network; + +import dev.lumareth.improvedAerodynamics.flight.AutoTrimToggle; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +/** Sent when a player presses the auto trim key, toggles auto trim on the control node of the seat they sit in. */ +public final class ToggleAutoTrimPacket { + public void encode(FriendlyByteBuf buffer) { + } + + public static ToggleAutoTrimPacket decode(FriendlyByteBuf buffer) { + return new ToggleAutoTrimPacket(); + } + + public void handle(Supplier context) { + ServerPlayer player = context.get().getSender(); + if (player != null && ModList.get().isLoaded("valkyrien_warium")) { + AutoTrimToggle.toggle(player); + } + } +} diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 37471fe..9248f23 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -30,10 +30,18 @@ versionRange = "[2.4.11,)" ordering = "AFTER" side = "BOTH" -# Optional, for the planned Warium VS flight physics overhaul +# Optional, the flight physics changes only apply with Warium VS [[dependencies."${mod_id}"]] modId = "valkyrien_warium" mandatory = false versionRange = "[1.1.6,)" ordering = "AFTER" side = "BOTH" + +# Optional, vectored afterburner thrust is smoothed as well +[[dependencies."${mod_id}"]] +modId = "futurism" +mandatory = false +versionRange = "[0.0.4,)" +ordering = "AFTER" +side = "BOTH" diff --git a/src/main/resources/assets/improved_aerodynamics/lang/de_de.json b/src/main/resources/assets/improved_aerodynamics/lang/de_de.json new file mode 100644 index 0000000..357c0bd --- /dev/null +++ b/src/main/resources/assets/improved_aerodynamics/lang/de_de.json @@ -0,0 +1,9 @@ +{ + "key.categories.improved_aerodynamics": "Improved Aerodynamics", + "key.improved_aerodynamics.toggle_auto_trim": "Automatische Trimmung umschalten", + "message.improved_aerodynamics.auto_trim.on": "Automatische Trimmung an", + "message.improved_aerodynamics.auto_trim.off": "Automatische Trimmung aus", + "message.improved_aerodynamics.auto_trim.unavailable": "Automatische Trimmung an, sie braucht aber physicsRateAerodynamics in der Serverkonfiguration", + "message.improved_aerodynamics.auto_trim.no_seat": "Setz dich in einen Warium-Steuersitz, um die automatische Trimmung zu nutzen", + "message.improved_aerodynamics.auto_trim.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden" +} diff --git a/src/main/resources/assets/improved_aerodynamics/lang/en_us.json b/src/main/resources/assets/improved_aerodynamics/lang/en_us.json new file mode 100644 index 0000000..345f416 --- /dev/null +++ b/src/main/resources/assets/improved_aerodynamics/lang/en_us.json @@ -0,0 +1,9 @@ +{ + "key.categories.improved_aerodynamics": "Improved Aerodynamics", + "key.improved_aerodynamics.toggle_auto_trim": "Toggle auto trim", + "message.improved_aerodynamics.auto_trim.on": "Auto trim on", + "message.improved_aerodynamics.auto_trim.off": "Auto trim off", + "message.improved_aerodynamics.auto_trim.unavailable": "Auto trim on, but it needs physicsRateAerodynamics enabled in the server config", + "message.improved_aerodynamics.auto_trim.no_seat": "Sit in a Warium control seat to use auto trim", + "message.improved_aerodynamics.auto_trim.no_node": "This seat is not linked to a vehicle control node" +} diff --git a/src/main/resources/improved_aerodynamics.mixins.json b/src/main/resources/improved_aerodynamics.mixins.json new file mode 100644 index 0000000..2607e64 --- /dev/null +++ b/src/main/resources/improved_aerodynamics.mixins.json @@ -0,0 +1,21 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.lumareth.improvedAerodynamics.flight.mixin", + "plugin": "dev.lumareth.improvedAerodynamics.flight.MixinPlugin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "futurism.ForceInducedShipsMixin", + "warium.AirBrakeTickProcedureMixin", + "warium.ControlSurfaceTickProcedureMixin", + "warium.ForceInducedShipsMixin", + "warium.HorizontalStabilizerScriptProcedureMixin", + "warium.HorizontalWingScriptProcedureMixin", + "warium.VerticalAirBrakeTickProcedureMixin", + "warium.VerticalControlSurfaceTickProcedureMixin", + "warium.VerticalWingScriptProcedureMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}