Merge feature/flaps into flight-computer

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
Claude committed 2026-11-11 18:37:11 +01:00
commit 782df70a69
19 files changed
+526 -14

No files matched your search

@@ -55,6 +55,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
double roll;
double trimIntegral;
double trimOutput;
// Deflection of this node's flaps in radians, moves towards what the flap lever and airspeed allow
double flapAngle;
// Total pitch input the flight computer applied last substep, pilot and trim together
double computerOutput;
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
@@ -84,6 +86,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
trimOutput = data.autoTrim() ? 0.0 : trimIntegral;
savedTrim = trimIntegral;
computerOutput = pitch + trimOutput;
flapAngle = Math.toRadians(FlapSetting.of(data.flapSetting()).degrees());
}
}
@@ -241,9 +244,10 @@ public final class AeroState implements PhysicsBridge.Publishable {
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, ControlAxis.NONE, 0.0) : 0.0;
double flap = s.flap() && state != null ? state.flapAngle : 0.0;
if (improved) {
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, flap, weight, tuning, stalled[i], out);
if (commit) {
stalled[i] = nowStalled;
wingStalled |= nowStalled && s.type() == SurfaceType.WING;
@@ -263,7 +267,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
double delta = 0.05;
probe.clear();
if (improved) {
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
ImprovedAero.evaluate(s, v, w, r, input, flap, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, input, weight, probe);
}
@@ -271,7 +275,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
probe.clear();
double nudged = input(s, node, state, axis, delta);
if (improved) {
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
ImprovedAero.evaluate(s, v, w, r, nudged, flap, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
}
@@ -328,10 +332,25 @@ public final class AeroState implements PhysicsBridge.Publishable {
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
state.roll = moveTowards(state.roll, effective.roll(), step);
updateFlaps(state, node, worldVelocity.length(), dt);
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, torque, dt, improved && wingStalled);
}
}
/**
* Moves a node's flaps towards the angle its lever asks for. Above the setting's maximum speed the air pushes them
* back in, so a craft that is too fast for its flaps has them half retracted, and all the way at 25% more.
*/
private static void updateFlaps(NodeState state, NodeData node, double speed, double dt) {
FlapSetting setting = FlapSetting.of(node.flapSetting());
double target = Math.toRadians(setting.degrees());
double limit = setting.maxSpeed();
if (speed > limit) {
target *= Math.max(0.0, 1.0 - (speed - limit) / (0.25 * limit));
}
state.flapAngle = moveTowards(state.flapAngle, target, Math.toRadians(Config.FLAP_SLEW_DEGREES.get()) * dt);
}
static double moveTowards(double value, double target, double maxStep) {
if (Math.abs(target - value) <= maxStep) {
return target;
@@ -14,16 +14,17 @@ package dev.lumareth.improvedAerodynamics.flight;
* @param brakeInput air brakes only: Warium's input value, they ignore smoothing and trim
* @param aspectRatio span squared over area of the wing the block belongs to, 0 if not known yet
* @param wingGroup whether the wing the block belongs to contains WING blocks, not only control surfaces or stabilizers
* @param flap whether the block was made a flap with shift-right-click, it then also follows its node's flap setting
*/
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, double aspectRatio, boolean wingGroup) {
double brakeInput, double aspectRatio, boolean wingGroup, boolean flap) {
public static final long NO_NODE = Long.MIN_VALUE;
AeroSurface withWingShape(double aspectRatio, boolean wingGroup) {
return new AeroSurface(key, type, slab, x, y, z, facingX, facingZ, pressure, fluidFactor, speedMultiplier,
node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio, wingGroup);
node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio, wingGroup, flap);
}
/** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */
@@ -105,7 +105,7 @@ final class CursorPilot {
Math.abs(node.pitch()) > MANUAL_OVERRIDE ? node.pitch() : pitch,
Math.abs(node.yaw()) > MANUAL_OVERRIDE ? node.yaw() : yaw,
Math.abs(node.roll()) > MANUAL_OVERRIDE ? node.roll() : roll,
node.throttle(), node.autoTrim(), node.storedTrim());
node.throttle(), node.autoTrim(), node.storedTrim(), node.flapSetting());
}
/** Least squares for the three inputs whose responses add up to the torque, ridge-regularized so unused axes stay at zero. */
@@ -0,0 +1,37 @@
package dev.lumareth.improvedAerodynamics.flight;
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
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.block.entity.BlockEntity;
/** Server side of the flap keys: moves the flap lever of the control node the pilot's seat is linked to. */
public final class FlapControl {
private FlapControl() {
}
/** @param step +1 for more flaps, -1 for fewer */
public static void step(ServerPlayer player, int step) {
Entity vehicle = player.getVehicle();
if (!(vehicle instanceof SeatEntityEntity)) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps.no_seat"), true);
return;
}
BlockEntity node = AutoTrimToggle.findNode(vehicle.level(), vehicle.blockPosition());
if (node == null) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps.no_node"), true);
return;
}
CompoundTag tag = node.getPersistentData();
int current = FlapSetting.of(tag.getInt(WariumCapture.FLAP_SETTING_TAG)).ordinal();
int next = Math.max(0, Math.min(FlapSetting.values().length - 1, current + (step > 0 ? 1 : -1)));
if (next != current) {
tag.putInt(WariumCapture.FLAP_SETTING_TAG, next);
node.setChanged();
}
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps",
Component.translatable(FlapSetting.of(next).translationKey())), true);
}
}
@@ -0,0 +1,66 @@
package dev.lumareth.improvedAerodynamics.flight;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
/**
* Shift-right-click on a block in the {@code improved_aerodynamics:flappable} tag (Warium's horizontal control
* surfaces) with empty hands makes it a flap, or a normal control surface again. A flap follows the flap lever of the
* control node it is linked to, on top of the pilot's input.
*/
public final class FlapInteraction {
public static final TagKey<Block> FLAPPABLE = TagKey.create(Registries.BLOCK,
new ResourceLocation(ImprovedAerodynamics.MOD_ID, "flappable"));
private FlapInteraction() {
}
@SubscribeEvent
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
Player player = event.getEntity();
if (event.getHand() != InteractionHand.MAIN_HAND || !player.isShiftKeyDown() || player.isPassenger()
|| !player.getMainHandItem().isEmpty() || !player.getOffhandItem().isEmpty()) {
return;
}
Level level = event.getLevel();
BlockPos pos = event.getPos();
BlockState state = level.getBlockState(pos);
if (!state.is(FLAPPABLE)) {
return;
}
// Handled on both sides so the block's own right-click action does not run on either
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide));
if (level.isClientSide) {
return;
}
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity == null) {
return;
}
CompoundTag tag = blockEntity.getPersistentData();
boolean flap = !tag.getBoolean(WariumCapture.FLAP_TAG);
tag.putBoolean(WariumCapture.FLAP_TAG, flap);
blockEntity.setChanged();
if (flap && !tag.contains("ControlX")) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flap.unlinked"), true);
} else {
player.displayClientMessage(Component.translatable(flap
? "message.improved_aerodynamics.flap.on" : "message.improved_aerodynamics.flap.off"), true);
}
}
}
@@ -0,0 +1,38 @@
package dev.lumareth.improvedAerodynamics.flight;
import dev.lumareth.improvedAerodynamics.Config;
/** Positions of a control node's flap lever. */
public enum FlapSetting {
UP,
TAKEOFF,
LANDING;
/** The setting for a stored number, UP for anything that is not one. */
public static FlapSetting of(int ordinal) {
FlapSetting[] values = values();
return ordinal >= 0 && ordinal < values.length ? values[ordinal] : UP;
}
/** Deflection in degrees the flaps move to at this setting. */
double degrees() {
return switch (this) {
case UP -> 0.0;
case TAKEOFF -> Config.FLAP_TAKEOFF_DEGREES.get();
case LANDING -> Config.FLAP_LANDING_DEGREES.get();
};
}
/** Airspeed in blocks per second above which the air starts to push the flaps back in. Infinite when they are up. */
double maxSpeed() {
return switch (this) {
case UP -> Double.POSITIVE_INFINITY;
case TAKEOFF -> Config.FLAP_TAKEOFF_MAX_SPEED.get();
case LANDING -> Config.FLAP_LANDING_MAX_SPEED.get();
};
}
public String translationKey() {
return "message.improved_aerodynamics.flaps." + name().toLowerCase(java.util.Locale.ROOT);
}
}
@@ -27,6 +27,12 @@ final class ImprovedAero {
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;
// How much of the lift a flap adds raises the stall angle, and the parasitic drag coefficient per radian² of flap
// Share of the flap's deflection that acts as angle of attack: 35 degrees of landing flap adds about 1.1 to the
// lift coefficient, like a real plain or slotted flap
private static final double FLAP_ALPHA_FACTOR = 0.4;
private static final double FLAP_STALL_GAIN = 0.7;
private static final double FLAP_DRAG = 0.35;
// Angle of attack of a level cambered wing, where K is calibrated
private static final double REFERENCE_ANGLE = Math.toRadians(4.0);
private static final double REFERENCE_SPEED = 40.0;
@@ -62,10 +68,11 @@ final class ImprovedAero {
* @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 flap flap deflection in radians, 0 for surfaces that are not flaps
* @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,
static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double flap, double weight,
Tuning tuning, 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];
@@ -114,6 +121,10 @@ final class ImprovedAero {
// 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);
}
// A flap bends the airfoil's camber whichever way the air comes, so it always adds lift, and it moves the stall
// up with it: a flap raises the highest lift a wing can make
double flapShift = FLAP_ALPHA_FACTOR * flap;
alpha += flapShift;
alpha = Math.max(-Math.PI / 2.0, Math.min(Math.PI / 2.0, alpha));
// Low aspect ratio wings keep their flow attached longer thanks to leading-edge vortices. Control surfaces
@@ -124,6 +135,9 @@ final class ImprovedAero {
: s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle;
double critical = Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
double absAlpha = Math.abs(alpha);
if (alpha >= 0.0) {
critical += FLAP_STALL_GAIN * flapShift;
}
// Flips only where both blends agree, so the hysteresis never makes the force jump
if (!stalled && absAlpha > critical + STALL_BLEND) {
stalled = true;
@@ -156,7 +170,7 @@ final class ImprovedAero {
double dy = -(vn * ny) / planeSpeed;
double dz = -(u * cz + vn * nz) / planeSpeed;
double liftForce = q * lift * tuning.liftScale();
double dragForce = q * drag / m;
double dragForce = q * (drag + FLAP_DRAG * flap * flap) / m;
out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce);
return stalled;
}
@@ -5,6 +5,7 @@ package dev.lumareth.improvedAerodynamics.flight;
* trim state.
*
* @param storedTrim trim saved in the node, used when the physics thread first sees the node
* @param flapSetting the node's flap lever, see {@link FlapSetting#ordinal()}
*/
public record NodeData(long key, double pitch, double yaw, double roll, double throttle, boolean autoTrim, double storedTrim) {
public record NodeData(long key, double pitch, double yaw, double roll, double throttle, boolean autoTrim, double storedTrim, int flapSetting) {
}
@@ -31,6 +31,9 @@ import org.valkyrienskies.mod.common.VSGameUtilsKt;
public final class WariumCapture {
public static final String AUTO_TRIM_TAG = "improved_aerodynamics_autotrim";
public static final String TRIM_TAG = "improved_aerodynamics_trim";
/** Block entity flag of a surface that is a flap, and the flap lever of a control node. */
public static final String FLAP_TAG = "improved_aerodynamics_flap";
public static final String FLAP_SETTING_TAG = "improved_aerodynamics_flap_setting";
private static final int TRIM_SAVE_INTERVAL = 20;
// The procedure currently running, procedures run one at a time on the server thread
@@ -77,10 +80,12 @@ public final class WariumCapture {
String key = "Null";
String key2 = "Null";
double brakeInput = 0.0;
boolean flap = false;
if (type.controllable || type == SurfaceType.AIR_BRAKE) {
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity != null) {
CompoundTag tag = blockEntity.getPersistentData();
flap = type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(FLAP_TAG);
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"));
@@ -97,7 +102,7 @@ public final class WariumCapture {
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, 0.0, false);
brakeInput, 0.0, false, flap);
}
/** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */
@@ -119,7 +124,8 @@ public final class WariumCapture {
}
}
aero.addNode(new NodeData(nodeKey, tag.getDouble("Pitch"), tag.getDouble("Yaw"), tag.getDouble("Roll"),
tag.getDouble("Throttle"), autoTrim, storedTrim));
tag.getDouble("Throttle"), autoTrim, storedTrim,
FlapSetting.of(tag.getInt(FLAP_SETTING_TAG)).ordinal()));
}
/** Raw node value for a key like "Pitch+", as Warium's air brakes read it. */