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:
1 parent
01817fbec1
commit
4b7fb0e1e8
37 files changed
+1954
-4
No files matched your search
@@ -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];
|
||||
}
|
||||
}
|
||||
+84
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user