Add flight physics overhaul with Warium/Futurism mixins

- Route Warium and Futurism force queues through smoothed per-substep delivery
- Evaluate wing, stabilizer, control surface and air brake forces every physics substep
- Add optional improved flight model with angle-of-attack lift and stalls
- Add auto trim toggled by a client key and a server packet
- Add config options, translations, and mixin plugin version gating
- Bump mod version to 0.2.0 and bundle Futurism 0.0.4 as a compile-only dependency
This commit is contained in:
richard committed 2026-10-28 18:56:25 +01:00
1 parent 01817fbec1
commit 4b7fb0e1e8
37 files changed
+1954 -4

No files matched your search

+9 -1
View File
@@ -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'
}
+2 -2
View File
@@ -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.
Binary file not shown.
@@ -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();
}
@@ -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();
}
}
@@ -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());
}
}
}
}
}
@@ -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();
}
@@ -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.
* <p>
* 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<AeroSurface> buildingSurfaces = new Long2ObjectOpenHashMap<>();
private Long2ObjectOpenHashMap<NodeData> 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<Long, NodeState> nodeStates = new ConcurrentHashMap<>();
private final Wrench wrench = new Wrench();
private final Wrench probe = new Wrench();
private record Frame(long tick, AeroSurface[] surfaces, Long2ObjectOpenHashMap<NodeData> 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);
}
}
@@ -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);
}
}
@@ -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).
* <p>
* 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));
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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.
* <p>
* 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<double[]> 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()));
}
}
}
@@ -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.
* <p>
* 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.
* <p>
* 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);
}
}
@@ -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<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> 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) {
}
}
@@ -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) {
}
@@ -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<Publishable> 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();
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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];
}
}
@@ -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<Pair<BlockPos, ForceData>> nextTickForces;
@Shadow
public Stack<RotData> 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();
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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<Pair<BlockPos, ForceData>> nextTickForces;
@Shadow
public Stack<RotData> 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();
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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<NetworkEvent.Context> context) {
ServerPlayer player = context.get().getSender();
if (player != null && ModList.get().isLoaded("valkyrien_warium")) {
AutoTrimToggle.toggle(player);
}
}
}
+9 -1
View File
@@ -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"
@@ -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"
}
@@ -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"
}
@@ -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
}
}