Add LiveInputs so nozzles and Futurism afterburners follow cursor aim
- Cursor aim reverses below the nose and eases off in stalls, with pitch and yaw signs learned from the measured responses - Cursor aim takes over at low speed and flies the surfaces directly while taxiing - Nozzles and Futurism vectored afterburners follow the live flight inputs instead of the raw keys - Warframes vertical surfaces and Futurism vectored afterburners are animated from their real deflection
This commit is contained in:
1 parent
3478a04885
commit
80bb63d44f
12 files changed
+365
-58
No files matched your search
+1
-1
@@ -12,7 +12,7 @@ mapping_version=2023.09.03-1.20.1
|
|||||||
mod_id=improved_aerodynamics
|
mod_id=improved_aerodynamics
|
||||||
mod_name=VS: Improved Aerodynamics
|
mod_name=VS: Improved Aerodynamics
|
||||||
mod_license=MIT
|
mod_license=MIT
|
||||||
mod_version=0.4.2
|
mod_version=0.4.3
|
||||||
azurelib_version=3.2.7
|
azurelib_version=3.2.7
|
||||||
mod_group_id=dev.lumareth.improvedaerodynamics
|
mod_group_id=dev.lumareth.improvedaerodynamics
|
||||||
mod_authors=Lumareth
|
mod_authors=Lumareth
|
||||||
|
|||||||
@@ -239,10 +239,15 @@ public final class Config {
|
|||||||
"negative g is harder on the craft and the pilot")
|
"negative g is harder on the craft and the pilot")
|
||||||
.defineInRange("pushFactor", 0.4, 0.0, 1.0);
|
.defineInRange("pushFactor", 0.4, 0.0, 1.0);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_REVERSE_TURN_DEGREES = BUILDER.comment("How far below the nose (wings level) the cursor has to be before the pilot rolls over and pulls towards it",
|
||||||
|
"instead of pushing the nose down. Less than that is flown with the elevators alone, like for a landing")
|
||||||
|
.defineInRange("reverseTurnDegrees", 40.0, 10.0, 135.0);
|
||||||
|
|
||||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_STALLED_PITCH_FACTOR = BUILDER.comment("Multiplier for the pitch rate the pilot asks for while the wings are stalled, so it does not keep pulling")
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_STALLED_PITCH_FACTOR = BUILDER.comment("Multiplier for the pitch rate the pilot asks for while the wings are stalled, so it does not keep pulling")
|
||||||
.defineInRange("stalledPitchFactor", 0.3, 0.0, 1.0);
|
.defineInRange("stalledPitchFactor", 0.3, 0.0, 1.0);
|
||||||
|
|
||||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MIN_SPEED = BUILDER.comment("Below this speed in blocks per second the pilot does nothing, the surfaces have no effect anyway")
|
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MIN_SPEED = BUILDER.comment("Below this speed in blocks per second the pilot does not fly the craft: the cursor moves the elevators and",
|
||||||
|
"rudder directly instead, so they follow it while taxiing and rotating for takeoff")
|
||||||
.defineInRange("minSpeed", 15.0, 0.0, 200.0);
|
.defineInRange("minSpeed", 15.0, 0.0, 200.0);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import net.minecraftforge.fml.ModList;
|
|||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
|
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
|
||||||
import org.joml.Matrix4f;
|
import org.joml.Matrix4f;
|
||||||
|
import org.joml.primitives.AABBdc;
|
||||||
import org.joml.Quaterniondc;
|
import org.joml.Quaterniondc;
|
||||||
import org.joml.Vector3d;
|
import org.joml.Vector3d;
|
||||||
import org.joml.Vector3f;
|
import org.joml.Vector3f;
|
||||||
@@ -64,6 +65,9 @@ public final class CursorAimClient {
|
|||||||
private static final float MARKER_DISTANCE = 2.0F;
|
private static final float MARKER_DISTANCE = 2.0F;
|
||||||
private static final double LOOK_AHEAD_RATE = 6.0;
|
private static final double LOOK_AHEAD_RATE = 6.0;
|
||||||
private static final double FREE_LOOK_RETURN_RATE = 10.0;
|
private static final double FREE_LOOK_RETURN_RATE = 10.0;
|
||||||
|
// Blocks under the craft's bounding box that count as still on the ground, and how long it has to be off it
|
||||||
|
private static final double GROUND_CLEARANCE = 1.5;
|
||||||
|
private static final int AIRBORNE_TICKS = 20;
|
||||||
|
|
||||||
private static boolean active;
|
private static boolean active;
|
||||||
// Chosen with /aero controls, told by the server
|
// Chosen with /aero controls, told by the server
|
||||||
@@ -76,6 +80,8 @@ public final class CursorAimClient {
|
|||||||
private static double cu;
|
private static double cu;
|
||||||
// Camera direction in ship space and the free look offset on top of it, degrees
|
// Camera direction in ship space and the free look offset on top of it, degrees
|
||||||
private static double[] cameraDirection;
|
private static double[] cameraDirection;
|
||||||
|
// Client ticks the craft has been off the ground, up to AIRBORNE_TICKS
|
||||||
|
private static int airborneTicks;
|
||||||
private static double offsetYaw;
|
private static double offsetYaw;
|
||||||
private static double offsetPitch;
|
private static double offsetPitch;
|
||||||
// Rotation the player was given last frame, to see what the mouse did since
|
// Rotation the player was given last frame, to see what the mouse did since
|
||||||
@@ -120,9 +126,51 @@ public final class CursorAimClient {
|
|||||||
if (rotation != null) {
|
if (rotation != null) {
|
||||||
cursorFromWorld(basis, rotation);
|
cursorFromWorld(basis, rotation);
|
||||||
}
|
}
|
||||||
|
updateAirborne(minecraft.level, seat);
|
||||||
send(minecraft, true);
|
send(minecraft, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the craft is off the ground, for look ahead: on the ground (and for a moment after leaving it) the camera
|
||||||
|
* stays behind the reticle, so pulling the nose up to take off does not swing it down under the craft.
|
||||||
|
*/
|
||||||
|
private static void updateAirborne(ClientLevel level, Entity seat) {
|
||||||
|
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
||||||
|
if (ship == null || onGround(level, ship.getWorldAABB())) {
|
||||||
|
airborneTicks = 0;
|
||||||
|
} else if (airborneTicks < AIRBORNE_TICKS) {
|
||||||
|
airborneTicks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether there is solid ground right under the craft: any block with a collision shape in a layer just below the
|
||||||
|
* bottom of its world bounding box. The craft's own blocks are not in the world, so they never count.
|
||||||
|
*/
|
||||||
|
private static boolean onGround(ClientLevel level, AABBdc box) {
|
||||||
|
int minX = Mth.floor(box.minX());
|
||||||
|
int maxX = Mth.floor(box.maxX());
|
||||||
|
int minZ = Mth.floor(box.minZ());
|
||||||
|
int maxZ = Mth.floor(box.maxZ());
|
||||||
|
int fromY = Mth.floor(box.minY() - GROUND_CLEARANCE);
|
||||||
|
int toY = Mth.floor(box.minY() + 0.5);
|
||||||
|
// At most about 16 x 16 columns, a big craft is sampled on a coarser grid
|
||||||
|
int stepX = Math.max(1, (maxX - minX) / 16);
|
||||||
|
int stepZ = Math.max(1, (maxZ - minZ) / 16);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int x = minX; x <= maxX; x += stepX) {
|
||||||
|
for (int z = minZ; z <= maxZ; z += stepZ) {
|
||||||
|
for (int y = fromY; y <= toY; y++) {
|
||||||
|
pos.set(x, y, z);
|
||||||
|
if (!level.getBlockState(pos).getCollisionShape(level, pos).isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** Rotation of the ship the seat is on, its render transform when drawing, null if it is not on a ship. */
|
/** Rotation of the ship the seat is on, its render transform when drawing, null if it is not on a ship. */
|
||||||
private static Quaterniondc shipRotation(ClientLevel level, Entity seat, boolean render) {
|
private static Quaterniondc shipRotation(ClientLevel level, Entity seat, boolean render) {
|
||||||
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
||||||
@@ -184,6 +232,7 @@ public final class CursorAimClient {
|
|||||||
cr = 0.0;
|
cr = 0.0;
|
||||||
cu = 0.0;
|
cu = 0.0;
|
||||||
cameraDirection = null;
|
cameraDirection = null;
|
||||||
|
airborneTicks = 0;
|
||||||
offsetYaw = 0.0;
|
offsetYaw = 0.0;
|
||||||
offsetPitch = 0.0;
|
offsetPitch = 0.0;
|
||||||
haveLast = false;
|
haveLast = false;
|
||||||
@@ -272,13 +321,14 @@ public final class CursorAimClient {
|
|||||||
}
|
}
|
||||||
cursorToWorld(basis, rotation);
|
cursorToWorld(basis, rotation);
|
||||||
|
|
||||||
// Camera: along the reticle, or turned towards the cursor by the look ahead setting
|
// Camera: along the reticle, or turned towards the cursor by the look ahead setting once the craft is flying
|
||||||
double lookAhead = Config.CURSOR_AIM_LOOK_AHEAD.get();
|
double lookAhead = Config.CURSOR_AIM_LOOK_AHEAD.get();
|
||||||
double[] forward = basis.forward();
|
double[] forward = basis.forward();
|
||||||
if (cameraDirection == null || lookAhead <= 0.0) {
|
if (cameraDirection == null || lookAhead <= 0.0) {
|
||||||
cameraDirection = forward.clone();
|
cameraDirection = forward.clone();
|
||||||
} else {
|
} else {
|
||||||
double[] target = basis.toShip(cf, cr, cu);
|
// On the ground it eases back behind the reticle rather than snapping there
|
||||||
|
double[] target = airborneTicks >= AIRBORNE_TICKS ? basis.toShip(cf, cr, cu) : forward;
|
||||||
cameraDirection = turnTowards(cameraDirection, target, 1.0 - Math.exp(-lookAhead * LOOK_AHEAD_RATE * dt));
|
cameraDirection = turnTowards(cameraDirection, target, 1.0 - Math.exp(-lookAhead * LOOK_AHEAD_RATE * dt));
|
||||||
}
|
}
|
||||||
double[] direction = rotate(cameraDirection, basis.up(), -Math.toRadians(offsetYaw));
|
double[] direction = rotate(cameraDirection, basis.up(), -Math.toRadians(offsetYaw));
|
||||||
|
|||||||
+62
-8
@@ -16,12 +16,23 @@ import software.bernie.geckolib.model.GeoModel;
|
|||||||
* bone of Warium's control surface models to them after GeckoLib has played Warium's own fixed animation.
|
* bone of Warium's control surface models to them after GeckoLib has played Warium's own fixed animation.
|
||||||
* <p>
|
* <p>
|
||||||
* Warium's animations tilt the Fin bone by 20 degrees about X for a positive input, which GeckoLib turns into a
|
* Warium's animations tilt the Fin bone by 20 degrees about X for a positive input, which GeckoLib turns into a
|
||||||
* rotation of -20 degrees (it negates X for rotations), and the same sign is used here, so a surface moves the way
|
* rotation of -20 degrees (it negates X and Y for rotations), and the same sign is used here, so a surface moves the
|
||||||
* Warium's own animation moves it, only by the amount the physics uses.
|
* way Warium's own animation moves it, only by the amount the physics uses. Warframes' vertical control surfaces are
|
||||||
|
* modelled differently: their Fin turns about Y, by -20 degrees in the animation file (+20 after GeckoLib) for the same
|
||||||
|
* input.
|
||||||
|
* <p>
|
||||||
|
* Futurism's vectored afterburner is shown the same way: its own animation only swings the Nozzle bone all the way (20
|
||||||
|
* degrees about X) up or down, here it follows the tilt the block actually has, which Futurism keeps in the block's
|
||||||
|
* synced data as "Vectorpitch" (minus a third of the input).
|
||||||
*/
|
*/
|
||||||
@Mod.EventBusSubscriber(modid = dev.lumareth.improvedAerodynamics.ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
|
@Mod.EventBusSubscriber(modid = dev.lumareth.improvedAerodynamics.ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
|
||||||
public final class SurfaceAnimation {
|
public final class SurfaceAnimation {
|
||||||
private static final String BONE = "Fin";
|
private static final String BONE = "Fin";
|
||||||
|
private static final String WARFRAMES_VERTICAL = "net.evo_mc.warframes.block.entity.WarframeVerticalControlSurfaceBlockEntity";
|
||||||
|
private static final String FUTURISM_VECTORED = "net.mcreator.futurism.block.entity.VectoredAfterburnerTileEntity";
|
||||||
|
private static final String NOZZLE_BONE = "Nozzle";
|
||||||
|
// Futurism's animation swing at full input, degrees
|
||||||
|
private static final float FUTURISM_FULL_DEGREES = 20.0F;
|
||||||
// A value that has not been refreshed for this long is dropped, Warium's own animation takes over again
|
// A value that has not been refreshed for this long is dropped, Warium's own animation takes over again
|
||||||
private static final long STALE_NANOS = 3_000_000_000L;
|
private static final long STALE_NANOS = 3_000_000_000L;
|
||||||
// Time in seconds the visible deflection takes to close most of the way to the latest value
|
// Time in seconds the visible deflection takes to close most of the way to the latest value
|
||||||
@@ -36,6 +47,8 @@ public final class SurfaceAnimation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final Long2ObjectOpenHashMap<Entry> ENTRIES = new Long2ObjectOpenHashMap<>();
|
private static final Long2ObjectOpenHashMap<Entry> ENTRIES = new Long2ObjectOpenHashMap<>();
|
||||||
|
// Shown angle (degrees) of each Futurism vectored afterburner
|
||||||
|
private static final Long2ObjectOpenHashMap<Entry> VECTORED = new Long2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
private SurfaceAnimation() {
|
private SurfaceAnimation() {
|
||||||
}
|
}
|
||||||
@@ -53,7 +66,14 @@ public final class SurfaceAnimation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void apply(BlockEntity blockEntity, GeoModel<?> model) {
|
public static void apply(BlockEntity blockEntity, GeoModel<?> model) {
|
||||||
if (ENTRIES.isEmpty() || !Config.ANIMATE_SURFACES.get()) {
|
if (!Config.ANIMATE_SURFACES.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (blockEntity.getClass().getName().equals(FUTURISM_VECTORED)) {
|
||||||
|
applyVectored(blockEntity, model);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ENTRIES.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Entry entry = ENTRIES.get(blockEntity.getBlockPos().asLong());
|
Entry entry = ENTRIES.get(blockEntity.getBlockPos().asLong());
|
||||||
@@ -65,20 +85,54 @@ public final class SurfaceAnimation {
|
|||||||
ENTRIES.remove(blockEntity.getBlockPos().asLong());
|
ENTRIES.remove(blockEntity.getBlockPos().asLong());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The same block can be drawn more than once a frame (shadows, other passes), the easing steps once per frame
|
ease(entry, now);
|
||||||
|
CoreGeoBone bone = model.getAnimationProcessor().getBone(BONE);
|
||||||
|
if (bone == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float radians = (float) Math.toRadians(entry.shown);
|
||||||
|
if (blockEntity.getClass().getName().equals(WARFRAMES_VERTICAL)) {
|
||||||
|
bone.setRotX(0.0F);
|
||||||
|
bone.setRotY(radians);
|
||||||
|
} else {
|
||||||
|
bone.setRotX(-radians);
|
||||||
|
}
|
||||||
|
// Setting a rotation flags the bone as animated, and GeckoLib cleared its flags before this runs. Left set, the
|
||||||
|
// flag would make GeckoLib skip resetting this bone for the next block drawn with the same model (all blocks of
|
||||||
|
// a model share its bones), so that block would keep this block's deflection
|
||||||
|
bone.resetStateChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Futurism's vectored afterburner: the Nozzle bone at the block's real tilt, Futurism's sign. */
|
||||||
|
private static void applyVectored(BlockEntity blockEntity, GeoModel<?> model) {
|
||||||
|
float input = (float) (-3.0 * blockEntity.getPersistentData().getDouble("Vectorpitch"));
|
||||||
|
float degrees = FUTURISM_FULL_DEGREES * Math.max(-1.0F, Math.min(1.0F, input));
|
||||||
|
Entry entry = VECTORED.computeIfAbsent(blockEntity.getBlockPos().asLong(), key -> new Entry());
|
||||||
|
entry.target = degrees;
|
||||||
|
if (!entry.placed) {
|
||||||
|
entry.shown = degrees;
|
||||||
|
entry.placed = true;
|
||||||
|
}
|
||||||
|
ease(entry, System.nanoTime());
|
||||||
|
CoreGeoBone bone = model.getAnimationProcessor().getBone(NOZZLE_BONE);
|
||||||
|
if (bone != null) {
|
||||||
|
bone.setRotX((float) -Math.toRadians(entry.shown));
|
||||||
|
bone.resetStateChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Moves the shown value towards the target. The same block can be drawn more than once a frame (shadows, other passes), the easing steps once per frame. */
|
||||||
|
private static void ease(Entry entry, long now) {
|
||||||
if (entry.drawn != 0L && now != entry.drawn) {
|
if (entry.drawn != 0L && now != entry.drawn) {
|
||||||
double dt = (now - entry.drawn) / 1.0E9;
|
double dt = (now - entry.drawn) / 1.0E9;
|
||||||
entry.shown += (entry.target - entry.shown) * (float) (1.0 - Math.exp(-dt / EASE_SECONDS));
|
entry.shown += (entry.target - entry.shown) * (float) (1.0 - Math.exp(-dt / EASE_SECONDS));
|
||||||
}
|
}
|
||||||
entry.drawn = now;
|
entry.drawn = now;
|
||||||
CoreGeoBone bone = model.getAnimationProcessor().getBone(BONE);
|
|
||||||
if (bone != null) {
|
|
||||||
bone.setRotX((float) -Math.toRadians(entry.shown));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) {
|
public static void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) {
|
||||||
ENTRIES.clear();
|
ENTRIES.clear();
|
||||||
|
VECTORED.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.exhaust;
|
package dev.lumareth.improvedAerodynamics.exhaust;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.flight.ControlAxis;
|
||||||
|
import dev.lumareth.improvedAerodynamics.flight.LiveInputs;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -13,7 +15,8 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
* with Warium's two key configuration screen, are read. Either key can name any axis: Pitch+ means W raises the nose and
|
* with Warium's two key configuration screen, are read. Either key can name any axis: Pitch+ means W raises the nose and
|
||||||
* Pitch- means S does, Roll+ or Roll- says which roll input rolls the plane right, Yaw+ or Yaw- which yaw input yaws it
|
* Pitch- means S does, Roll+ or Roll- says which roll input rolls the plane right, Yaw+ or Yaw- which yaw input yaws it
|
||||||
* right. An axis no key names is off, except that a nozzle with no key at all pitches with W, and yaw follows D without
|
* right. An axis no key names is off, except that a nozzle with no key at all pitches with W, and yaw follows D without
|
||||||
* a key.
|
* a key. While the ship flies, the inputs are the ones the flight physics uses ({@link LiveInputs}), so the nozzles follow
|
||||||
|
* cursor aim and the flight computer like the control surfaces do.
|
||||||
*/
|
*/
|
||||||
final class NozzleControl {
|
final class NozzleControl {
|
||||||
private static final ResourceLocation NODE_BLOCK = new ResourceLocation("valkyrien_warium", "vehicle_control_node");
|
private static final ResourceLocation NODE_BLOCK = new ResourceLocation("valkyrien_warium", "vehicle_control_node");
|
||||||
@@ -43,9 +46,10 @@ final class NozzleControl {
|
|||||||
String key = own.getString("Key");
|
String key = own.getString("Key");
|
||||||
String key2 = own.getString("Key2");
|
String key2 = own.getString("Key2");
|
||||||
boolean unset = (key.isEmpty() || key.equals("Null")) && (key2.isEmpty() || key2.equals("Null"));
|
boolean unset = (key.isEmpty() || key.equals("Null")) && (key2.isEmpty() || key2.equals("Null"));
|
||||||
double pitch = clamp(tag.getDouble("Pitch")) * sign(key, key2, "Pitch", unset ? 1.0 : 0.0);
|
long nodeKey = nodePos.asLong();
|
||||||
double roll = clamp(tag.getDouble("Roll")) * sign(key, key2, "Roll", 0.0);
|
double pitch = clamp(LiveInputs.value(nodeKey, tag, ControlAxis.PITCH)) * sign(key, key2, "Pitch", unset ? 1.0 : 0.0);
|
||||||
double yaw = clamp(tag.getDouble("Yaw")) * sign(key, key2, "Yaw", 1.0);
|
double roll = clamp(LiveInputs.value(nodeKey, tag, ControlAxis.ROLL)) * sign(key, key2, "Roll", 0.0);
|
||||||
|
double yaw = clamp(LiveInputs.value(nodeKey, tag, ControlAxis.YAW)) * sign(key, key2, "Yaw", 1.0);
|
||||||
return new Inputs(node, pitch, roll, yaw, Math.max(0.0, tag.getDouble("Throttle")), tag.getBoolean(AfterburnerToggle.TAG));
|
return new Inputs(node, pitch, roll, yaw, Math.max(0.0, tag.getDouble("Throttle")), tag.getBoolean(AfterburnerToggle.TAG));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
// Fastest steady pitch rate (rad/s) the pitch surfaces can hold against the craft's pitch damping, smoothed,
|
// Fastest steady pitch rate (rad/s) the pitch surfaces can hold against the craft's pitch damping, smoothed,
|
||||||
// measured while cursor aim flies the node. NaN until measured
|
// measured while cursor aim flies the node. NaN until measured
|
||||||
double controlRate = Double.NaN;
|
double controlRate = Double.NaN;
|
||||||
|
// Cursor aim is rolling the craft over to reach a cursor far below the nose
|
||||||
|
boolean reversing;
|
||||||
|
// Share of the pull cursor aim keeps, eased down while the wings are stalled
|
||||||
|
double stallEase = 1.0;
|
||||||
|
// Which way pitch and yaw input turn the craft (+1 or -1 for nose up and nose right), learned from the measured
|
||||||
|
// responses. Until then Warium's keys: W (Pitch+) is usually nose down, D (Yaw+) nose right
|
||||||
|
double pitchSign = -1.0;
|
||||||
|
double yawSign = 1.0;
|
||||||
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
||||||
volatile double savedTrim;
|
volatile double savedTrim;
|
||||||
|
|
||||||
@@ -384,7 +392,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
}
|
}
|
||||||
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys
|
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys
|
||||||
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled,
|
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled,
|
||||||
liftAcceleration);
|
liftAcceleration, dt);
|
||||||
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
||||||
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
||||||
state.roll = moveTowards(state.roll, effective.roll(), step);
|
state.roll = moveTowards(state.roll, effective.roll(), step);
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ import org.joml.Vector3d;
|
|||||||
* bank needs, so the craft turns without losing height;</li>
|
* bank needs, so the craft turns without losing height;</li>
|
||||||
* <li>it pitches towards the cursor as seen from the cockpit, so once banked the cursor is above the nose and the
|
* <li>it pitches towards the cursor as seen from the cockpit, so once banked the cursor is above the nose and the
|
||||||
* pull brings the nose round onto it;</li>
|
* pull brings the nose round onto it;</li>
|
||||||
|
* <li>a cursor far below the nose (more than {@code reverseTurnDegrees}, judged with the wings level) is not reached
|
||||||
|
* by pushing: the pilot rolls until the cursor is above the canopy and pulls, a reversal or split-S. It keeps at it
|
||||||
|
* until the reticle is close to the cursor, or the cursor is moved above the nose again. A cursor only a little below
|
||||||
|
* the nose is reached with the elevators alone, gently, so the craft can still be flown down to a landing;</li>
|
||||||
|
* <li>below {@code minSpeed} there is too little airflow to fly by: the cursor moves elevators and rudder directly;</li>
|
||||||
* <li>close to the reticle it levels the wings and puts the reticle on the cursor with rudder and elevator, so
|
* <li>close to the reticle it levels the wings and puts the reticle on the cursor with rudder and elevator, so
|
||||||
* aiming does not rock the wings.</li>
|
* aiming does not rock the wings.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@@ -38,6 +43,10 @@ final class CursorPilot {
|
|||||||
private static final double LIFT_MARGIN = 0.85;
|
private static final double LIFT_MARGIN = 0.85;
|
||||||
// Bank the pilot rolls to at full sideways deflection even when the wings cannot hold the turn level
|
// Bank the pilot rolls to at full sideways deflection even when the wings cannot hold the turn level
|
||||||
private static final double MIN_BANK = Math.toRadians(20.0);
|
private static final double MIN_BANK = Math.toRadians(20.0);
|
||||||
|
// Smallest pull allowed, as a share of the turn rate, when gravity takes most of the lift
|
||||||
|
private static final double MIN_PULL_SHARE = 0.2;
|
||||||
|
// Time constant of easing the pull back in and out of a stall
|
||||||
|
private static final double STALL_EASE_SECONDS = 0.4;
|
||||||
// Turn rate when neither limit can be measured, like the first moments of a flight
|
// Turn rate when neither limit can be measured, like the first moments of a flight
|
||||||
private static final double FALLBACK_TURN_RATE = Math.toRadians(30.0);
|
private static final double FALLBACK_TURN_RATE = Math.toRadians(30.0);
|
||||||
|
|
||||||
@@ -48,12 +57,12 @@ final class CursorPilot {
|
|||||||
* @return the node with the pilot's wanted inputs in place of the keys, or the node itself if cursor aim is off
|
* @return the node with the pilot's wanted inputs in place of the keys, or the node itself if cursor aim is off
|
||||||
*/
|
*/
|
||||||
static NodeData apply(AeroState.NodeState state, NodeData node, Quaterniondc rotation, Matrix3dc inertia, double[] w,
|
static NodeData apply(AeroState.NodeState state, NodeData node, Quaterniondc rotation, Matrix3dc inertia, double[] w,
|
||||||
double speed, boolean wingStalled, double liftAcceleration) {
|
double speed, boolean wingStalled, double liftAcceleration, double dt) {
|
||||||
if (!Config.CURSOR_AIM_ENABLED.get()) {
|
if (!Config.CURSOR_AIM_ENABLED.get()) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
CursorAimRegistry.Aim aim = CursorAimRegistry.get(node.key());
|
CursorAimRegistry.Aim aim = CursorAimRegistry.get(node.key());
|
||||||
if (aim == null || speed < Config.CURSOR_AIM_MIN_SPEED.get()) {
|
if (aim == null) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,24 +70,10 @@ final class CursorPilot {
|
|||||||
double maxRoll = Math.toRadians(Config.CURSOR_AIM_MAX_ROLL_RATE_DEGREES.get());
|
double maxRoll = Math.toRadians(Config.CURSOR_AIM_MAX_ROLL_RATE_DEGREES.get());
|
||||||
double maxBank = Math.toRadians(Config.CURSOR_AIM_MAX_BANK_DEGREES.get());
|
double maxBank = Math.toRadians(Config.CURSOR_AIM_MAX_BANK_DEGREES.get());
|
||||||
|
|
||||||
// What this craft can do right now. Lift: pitch rate at the highest usable lift (a = L / m, rate = a / v), and
|
|
||||||
// the bank whose level turn needs that load factor. Without a stall model only the control limit applies
|
|
||||||
double usableLift = LIFT_MARGIN * liftAcceleration;
|
|
||||||
double liftRate = usableLift > 0.0 ? usableLift / speed : Double.POSITIVE_INFINITY;
|
|
||||||
double controlRate = state.controlRate > 0.0 ? state.controlRate : Double.POSITIVE_INFINITY;
|
|
||||||
double maxTurn = Math.min(liftRate, controlRate);
|
|
||||||
if (!Double.isFinite(maxTurn)) {
|
|
||||||
maxTurn = FALLBACK_TURN_RATE;
|
|
||||||
}
|
|
||||||
if (Double.isFinite(liftAcceleration)) {
|
|
||||||
double loadFactor = usableLift / GRAVITY;
|
|
||||||
double levelBank = loadFactor > 1.0 ? Math.acos(1.0 / loadFactor) : 0.0;
|
|
||||||
maxBank = Math.max(Math.min(MIN_BANK, maxBank), Math.min(maxBank, levelBank));
|
|
||||||
}
|
|
||||||
|
|
||||||
double[] f = aim.forward();
|
double[] f = aim.forward();
|
||||||
double[] r = aim.right();
|
double[] r = aim.right();
|
||||||
double[] u = aim.up();
|
double[] u = aim.up();
|
||||||
|
learnSigns(state, f, r, u);
|
||||||
|
|
||||||
// Bank of the craft: positive when the right wing is down, through upside down
|
// Bank of the craft: positive when the right wing is down, through upside down
|
||||||
Vector3d rightWorld = rotation.transform(new Vector3d(r[0], r[1], r[2]));
|
Vector3d rightWorld = rotation.transform(new Vector3d(r[0], r[1], r[2]));
|
||||||
@@ -102,26 +97,85 @@ final class CursorPilot {
|
|||||||
// 270 degree cone
|
// 270 degree cone
|
||||||
double sideways = Math.atan2(cr, cf);
|
double sideways = Math.atan2(cr, cf);
|
||||||
double vertical = Math.atan2(cu, Math.hypot(cf, cr));
|
double vertical = Math.atan2(cu, Math.hypot(cf, cr));
|
||||||
// Sideways as it would be with the wings level, what the bank is chosen by
|
// The cursor as it would be with the wings level: what the bank is chosen by
|
||||||
double levelSideways = Math.atan2(cr * Math.cos(bank) + cu * Math.sin(bank), cf);
|
double levelRight = cr * Math.cos(bank) + cu * Math.sin(bank);
|
||||||
double blend = smoothstep(Math.abs(levelSideways), FINE_START, FINE_END);
|
double levelUp = cu * Math.cos(bank) - cr * Math.sin(bank);
|
||||||
|
double levelVertical = Math.atan2(levelUp, Math.hypot(cf, levelRight));
|
||||||
|
|
||||||
double bankCommand = maxBank * saturate(levelSideways / fullAngle) * blend;
|
if (speed < Config.CURSOR_AIM_MIN_SPEED.get()) {
|
||||||
double rollRate = clamp(Config.CURSOR_AIM_BANK_GAIN.get() * (bankCommand - bank), -maxRoll, maxRoll);
|
// Too slow for the rate loop: the cursor moves the surfaces directly, rudder towards it (which also steers on
|
||||||
|
// the ground) and elevator up or down, so the controls follow the cursor while taxiing and rotating
|
||||||
|
state.reversing = false;
|
||||||
|
double pitch = clamp(state.pitchSign * saturate(vertical / fullAngle) - state.trimOutput, -1.0, 1.0);
|
||||||
|
double yaw = state.yawSign * saturate(sideways / fullAngle);
|
||||||
|
return withInputs(node, pitch, yaw, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What this craft can do right now. Lift: pitch rate at the highest usable lift (a = L / m, rate = a / v), and
|
||||||
|
// the bank whose level turn needs that load factor. Without a stall model only the control limit applies
|
||||||
|
double usableLift = LIFT_MARGIN * liftAcceleration;
|
||||||
|
double liftRate = usableLift > 0.0 ? usableLift / speed : Double.POSITIVE_INFINITY;
|
||||||
|
double controlRate = state.controlRate > 0.0 ? state.controlRate : Double.POSITIVE_INFINITY;
|
||||||
|
double maxTurn = Math.min(liftRate, controlRate);
|
||||||
|
if (!Double.isFinite(maxTurn)) {
|
||||||
|
maxTurn = FALLBACK_TURN_RATE;
|
||||||
|
}
|
||||||
|
// Fastest pull: part of the lift already holds the craft up against gravity, only the rest curves the flight
|
||||||
|
// path, so asking for the full lift rate in a steep bank would drag the wings into the stall
|
||||||
|
double maxPull = maxTurn;
|
||||||
|
if (Double.isFinite(liftAcceleration)) {
|
||||||
|
double loadFactor = usableLift / GRAVITY;
|
||||||
|
double levelBank = loadFactor > 1.0 ? Math.acos(1.0 / loadFactor) : 0.0;
|
||||||
|
maxBank = Math.max(Math.min(MIN_BANK, maxBank), Math.min(maxBank, levelBank));
|
||||||
|
double pullLift = (usableLift - GRAVITY * upWorld.y) / speed;
|
||||||
|
maxPull = clamp(Math.min(pullLift, controlRate), MIN_PULL_SHARE * maxTurn, maxTurn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reversal: entered when the cursor is far below the nose, left once the reticle is close to it or the cursor
|
||||||
|
// is above the nose again
|
||||||
|
double offsetAngle = Math.acos(clamp(cf, -1.0, 1.0));
|
||||||
|
if (state.reversing) {
|
||||||
|
state.reversing = offsetAngle > FINE_END && levelVertical < 0.0;
|
||||||
|
} else {
|
||||||
|
state.reversing = levelVertical < -Math.toRadians(Config.CURSOR_AIM_REVERSE_TURN_DEGREES.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bank: roll so the cursor is above the canopy, which is where a pull takes the nose. Judged by the direction of
|
||||||
|
// the cursor around the nose, which stays well defined wherever the cursor is, even 90 degrees off the nose
|
||||||
|
// straight above or to the side; the angle of the cursor from the nose only sets how much of that bank is used,
|
||||||
|
// so the wings come level as the reticle closes on the cursor
|
||||||
|
double blend = state.reversing ? 1.0 : smoothstep(offsetAngle, FINE_START, FINE_END);
|
||||||
|
double bankError;
|
||||||
|
if (state.reversing) {
|
||||||
|
// All the way over, the shorter way round
|
||||||
|
bankError = wrap(Math.atan2(levelRight, levelUp) - bank);
|
||||||
|
} else {
|
||||||
|
// A cursor below the nose counts as if it were as far above it: only its sideways part banks the craft, and
|
||||||
|
// the craft is never rolled upside down for a slight descent
|
||||||
|
double overhead = Math.atan2(levelRight, Math.abs(levelUp));
|
||||||
|
double bankCommand = clamp(overhead, -maxBank, maxBank) * saturate(offsetAngle / fullAngle) * blend;
|
||||||
|
bankError = bankCommand - bank;
|
||||||
|
}
|
||||||
|
double rollRate = clamp(Config.CURSOR_AIM_BANK_GAIN.get() * bankError, -maxRoll, maxRoll);
|
||||||
|
|
||||||
// Pitch: the pull of a level turn at the current bank, the turn rate g tan(bank) / v seen about the pitch axis,
|
// Pitch: the pull of a level turn at the current bank, the turn rate g tan(bank) / v seen about the pitch axis,
|
||||||
// plus a pull towards the cursor. Past 90 degrees of bank the level turn pull would only dive, so it is left out
|
// plus a pull towards the cursor. Past 90 degrees of bank the level turn pull would only dive, so it is left out
|
||||||
double pitchStick = saturate(vertical / fullAngle);
|
double pitchStick = saturate(vertical / fullAngle);
|
||||||
double push = Config.CURSOR_AIM_PUSH_FACTOR.get();
|
double push = Config.CURSOR_AIM_PUSH_FACTOR.get();
|
||||||
double pitchRate = maxTurn * (pitchStick >= 0.0 ? pitchStick : pitchStick * push);
|
// While rolling into a reversal the cursor is still below the nose, pushing there would only work against it
|
||||||
|
double pitchRate = pitchStick >= 0.0 ? maxPull * pitchStick : state.reversing ? 0.0 : maxTurn * pitchStick * push;
|
||||||
double cosBank = Math.cos(bank);
|
double cosBank = Math.cos(bank);
|
||||||
if (cosBank > 0.0) {
|
if (cosBank > 0.0) {
|
||||||
double sinBank = Math.sin(bank);
|
double sinBank = Math.sin(bank);
|
||||||
pitchRate += GRAVITY * sinBank * sinBank / (speed * Math.max(cosBank, 0.1));
|
pitchRate += GRAVITY * sinBank * sinBank / (speed * Math.max(cosBank, 0.1));
|
||||||
}
|
}
|
||||||
pitchRate = clamp(pitchRate, -maxTurn * push, maxTurn);
|
// Stalled wings ease the pull back, and it comes back as gently once they fly again: switching it at once with
|
||||||
if (wingStalled && pitchRate > 0.0) {
|
// the stall would make the craft buck in and out of the stall
|
||||||
pitchRate *= Config.CURSOR_AIM_STALLED_PITCH_FACTOR.get();
|
double stallTarget = wingStalled ? Config.CURSOR_AIM_STALLED_PITCH_FACTOR.get() : 1.0;
|
||||||
|
state.stallEase += (stallTarget - state.stallEase) * Math.min(1.0, dt / STALL_EASE_SECONDS);
|
||||||
|
pitchRate = clamp(pitchRate, -maxTurn * push, maxPull);
|
||||||
|
if (pitchRate > 0.0) {
|
||||||
|
pitchRate *= state.stallEase;
|
||||||
}
|
}
|
||||||
// Close to the reticle the nose follows the cursor with the rudder
|
// Close to the reticle the nose follows the cursor with the rudder
|
||||||
double yawRate = maxTurn * saturate(sideways / fullAngle) * (1.0 - blend);
|
double yawRate = maxTurn * saturate(sideways / fullAngle) * (1.0 - blend);
|
||||||
@@ -143,6 +197,11 @@ final class CursorPilot {
|
|||||||
double pitch = clamp(inputs[0] - state.trimOutput, -1.0, 1.0);
|
double pitch = clamp(inputs[0] - state.trimOutput, -1.0, 1.0);
|
||||||
double yaw = clamp(inputs[1], -1.0, 1.0);
|
double yaw = clamp(inputs[1], -1.0, 1.0);
|
||||||
double roll = clamp(inputs[2], -1.0, 1.0);
|
double roll = clamp(inputs[2], -1.0, 1.0);
|
||||||
|
return withInputs(node, pitch, yaw, roll);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The node with these inputs on the axes the pilot is not holding a key for. */
|
||||||
|
private static NodeData withInputs(NodeData node, double pitch, double yaw, double roll) {
|
||||||
return new NodeData(node.key(),
|
return new NodeData(node.key(),
|
||||||
Math.abs(node.pitch()) > MANUAL_OVERRIDE ? node.pitch() : pitch,
|
Math.abs(node.pitch()) > MANUAL_OVERRIDE ? node.pitch() : pitch,
|
||||||
Math.abs(node.yaw()) > MANUAL_OVERRIDE ? node.yaw() : yaw,
|
Math.abs(node.yaw()) > MANUAL_OVERRIDE ? node.yaw() : yaw,
|
||||||
@@ -150,6 +209,25 @@ final class CursorPilot {
|
|||||||
node.throttle(), node.autoTrim(), node.storedTrim(), node.flapSetting());
|
node.throttle(), node.autoTrim(), node.storedTrim(), node.flapSetting());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers which way each input turns the craft, from the measured torque per unit of input: + nose up for pitch,
|
||||||
|
* nose right for yaw. Kept from the last time it was measurable, since with no airflow nothing can be measured.
|
||||||
|
*/
|
||||||
|
private static void learnSigns(AeroState.NodeState state, double[] f, double[] r, double[] u) {
|
||||||
|
state.pitchSign = sign(state.pitchResponse, r, 1.0, state.pitchSign);
|
||||||
|
state.yawSign = sign(state.yawResponse, u, -1.0, state.yawSign);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double sign(Vector3d response, double[] axis, double direction, double previous) {
|
||||||
|
double length = response.length();
|
||||||
|
if (!(length > 1.0E-6)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
double along = direction * (response.x * axis[0] + response.y * axis[1] + response.z * axis[2]) / length;
|
||||||
|
// Only trusted while the input clearly turns the craft about that axis
|
||||||
|
return Math.abs(along) > 0.3 ? Math.signum(along) : previous;
|
||||||
|
}
|
||||||
|
|
||||||
/** Least squares for the three inputs whose responses add up to the torque, ridge-regularized so unused axes stay at zero. */
|
/** Least squares for the three inputs whose responses add up to the torque, ridge-regularized so unused axes stay at zero. */
|
||||||
private static double[] solve(Vector3d pitch, Vector3d yaw, Vector3d roll, Vector3d torque) {
|
private static double[] solve(Vector3d pitch, Vector3d yaw, Vector3d roll, Vector3d torque) {
|
||||||
Matrix3d responses = new Matrix3d(pitch, yaw, roll);
|
Matrix3d responses = new Matrix3d(pitch, yaw, roll);
|
||||||
@@ -171,6 +249,11 @@ final class CursorPilot {
|
|||||||
return new double[]{x.x, x.y, x.z};
|
return new double[]{x.x, x.y, x.z};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The angle brought into -pi to pi. */
|
||||||
|
private static double wrap(double angle) {
|
||||||
|
return Math.atan2(Math.sin(angle), Math.cos(angle));
|
||||||
|
}
|
||||||
|
|
||||||
private static double saturate(double value) {
|
private static double saturate(double value) {
|
||||||
return clamp(value, -1.0, 1.0);
|
return clamp(value, -1.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ public final class DeflectionSync {
|
|||||||
private static final double FULL_INPUT_DEGREES = 20.0;
|
private static final double FULL_INPUT_DEGREES = 20.0;
|
||||||
private static final long RESEND_TICKS = 20;
|
private static final long RESEND_TICKS = 20;
|
||||||
private static final double RANGE = 256.0;
|
private static final double RANGE = 256.0;
|
||||||
// A node state the physics thread has not touched for this long is not flying any more
|
|
||||||
private static final long FRESH_MILLIS = 500;
|
|
||||||
|
|
||||||
private record Batch(ServerLevel level, double x, double y, double z, LongArrayList positions, ShortArrayList tenths) {
|
private record Batch(ServerLevel level, double x, double y, double z, LongArrayList positions, ShortArrayList tenths) {
|
||||||
}
|
}
|
||||||
@@ -71,10 +69,10 @@ public final class DeflectionSync {
|
|||||||
}
|
}
|
||||||
long nodeKey = nodePos.asLong();
|
long nodeKey = nodePos.asLong();
|
||||||
CompoundTag nodeTag = nodeEntity.getPersistentData();
|
CompoundTag nodeTag = nodeEntity.getPersistentData();
|
||||||
AeroState.NodeState state = AeroState.nodeState(nodeKey);
|
AeroState.NodeState state = LiveInputs.flying(nodeKey);
|
||||||
boolean flying = state != null && System.currentTimeMillis() - state.updatedMillis < FRESH_MILLIS;
|
boolean flying = state != null;
|
||||||
|
|
||||||
double input = axis(tag.getString("Key"), nodeTag, state, flying) + axis(tag.getString("Key2"), nodeTag, state, flying);
|
double input = axisInput(nodeKey, nodeTag, tag.getString("Key")) + axisInput(nodeKey, nodeTag, tag.getString("Key2"));
|
||||||
double degrees = FULL_INPUT_DEGREES * Math.max(-1.0, Math.min(1.0, input));
|
double degrees = FULL_INPUT_DEGREES * Math.max(-1.0, Math.min(1.0, input));
|
||||||
if (type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(WariumCapture.FLAP_TAG)) {
|
if (type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(WariumCapture.FLAP_TAG)) {
|
||||||
degrees += Math.toDegrees(flying ? state.flapAngle : staticFlap(level, nodeKey, nodeTag));
|
degrees += Math.toDegrees(flying ? state.flapAngle : staticFlap(level, nodeKey, nodeTag));
|
||||||
@@ -107,16 +105,9 @@ public final class DeflectionSync {
|
|||||||
batch.tenths.add(tenths);
|
batch.tenths.add(tenths);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One key's share of a surface's input, like Warium reads "Pitch+" / "Roll-". */
|
/** One key's share of a surface's input, like Warium reads "Pitch+" / "Roll-". Throttle keys do not deflect it. */
|
||||||
private static double axis(String key, CompoundTag nodeTag, AeroState.NodeState state, boolean flying) {
|
private static double axisInput(long nodeKey, CompoundTag nodeTag, String key) {
|
||||||
ControlAxis axis = ControlAxis.of(key);
|
return ControlAxis.of(key) == ControlAxis.THROTTLE ? 0.0 : LiveInputs.key(nodeKey, nodeTag, key);
|
||||||
double multiplier = ControlAxis.multiplier(key);
|
|
||||||
return switch (axis) {
|
|
||||||
case PITCH -> (flying ? state.pitch + state.trimOutput : nodeTag.getDouble("Pitch")) * multiplier;
|
|
||||||
case YAW -> (flying ? state.yaw : nodeTag.getDouble("Yaw")) * multiplier;
|
|
||||||
case ROLL -> (flying ? state.roll : nodeTag.getDouble("Roll")) * multiplier;
|
|
||||||
default -> 0.0;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Flap angle of a node that is not flying: moves towards the lever's angle, once per tick however many flaps ask. */
|
/** Flap angle of a node that is not flying: moves towards the lever's angle, once per tick however many flaps ask. */
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package dev.lumareth.improvedAerodynamics.flight;
|
||||||
|
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pilot inputs a control node's devices should follow, for everything that is not an aerodynamic surface (thrust
|
||||||
|
* vectoring nozzles). While the node's ship is flying these are the inputs the flight physics uses: the pilot's keys
|
||||||
|
* smoothed, or what cursor aim asks for in their place, plus the flight computer's pitch trim. A node without physics
|
||||||
|
* (on the ground, nothing moving, physics rate aerodynamics off) gives Warium's raw key values, as before.
|
||||||
|
*/
|
||||||
|
public final class LiveInputs {
|
||||||
|
// A node state the physics thread has not touched for this long is not flying any more
|
||||||
|
private static final long FRESH_MILLIS = 500;
|
||||||
|
|
||||||
|
private LiveInputs() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The node's physics state if its ship is flying now, else null. */
|
||||||
|
static AeroState.NodeState flying(long nodeKey) {
|
||||||
|
AeroState.NodeState state = AeroState.nodeState(nodeKey);
|
||||||
|
return state != null && System.currentTimeMillis() - state.updatedMillis < FRESH_MILLIS ? state : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One axis of a node, in Warium's node units (-1 to 1), without any key multiplier.
|
||||||
|
*
|
||||||
|
* @param nodeTag the node block entity's persistent data, for the raw value
|
||||||
|
*/
|
||||||
|
public static double value(long nodeKey, CompoundTag nodeTag, ControlAxis axis) {
|
||||||
|
AeroState.NodeState state = flying(nodeKey);
|
||||||
|
return switch (axis) {
|
||||||
|
case PITCH -> state != null ? state.pitch + state.trimOutput : nodeTag.getDouble("Pitch");
|
||||||
|
case YAW -> state != null ? state.yaw : nodeTag.getDouble("Yaw");
|
||||||
|
case ROLL -> state != null ? state.roll : nodeTag.getDouble("Roll");
|
||||||
|
case THROTTLE -> nodeTag.getDouble("Throttle");
|
||||||
|
case NONE -> 0.0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A Warium key such as "Pitch+" or "Roll-" read from the node: its axis' value times the key's sign. */
|
||||||
|
public static double key(long nodeKey, CompoundTag nodeTag, String key) {
|
||||||
|
return value(nodeKey, nodeTag, ControlAxis.of(key)) * ControlAxis.multiplier(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
package dev.lumareth.improvedAerodynamics.flight.mixin.futurism;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.flight.LiveInputs;
|
||||||
|
import net.mcreator.futurism.procedures.VectoredAfterburnerSystemProcedure;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.LevelAccessor;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
|
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.ModifyVariable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Futurism's vectored afterburner reads the raw key values of its control node, so it ignored cursor aim and the flight
|
||||||
|
* computer. This hands it the same inputs the control surfaces follow ({@link LiveInputs}), through the same Key and Key2
|
||||||
|
* it is configured with. It stays what Futurism made it, 2D vectoring: the procedure still only tilts the thrust up or
|
||||||
|
* down. The value is kept on Warium's 0.2 key steps, since every change of it plays Futurism's servo sound and sends a
|
||||||
|
* block update.
|
||||||
|
*/
|
||||||
|
@Mixin(value = VectoredAfterburnerSystemProcedure.class, remap = false)
|
||||||
|
public abstract class VectoredAfterburnerSystemProcedureMixin {
|
||||||
|
private static final double STEP = 0.2;
|
||||||
|
|
||||||
|
@ModifyVariable(method = "execute", at = @At("STORE"), name = "input")
|
||||||
|
private static double improvedAerodynamics$liveInput(double input, LevelAccessor world, double x, double y, double z, BlockState blockstate) {
|
||||||
|
if (!(world instanceof ServerLevel level)) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
BlockEntity own = level.getBlockEntity(BlockPos.containing(x, y, z));
|
||||||
|
if (own == null) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
CompoundTag tag = own.getPersistentData();
|
||||||
|
BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ"));
|
||||||
|
BlockEntity node = level.getBlockEntity(nodePos);
|
||||||
|
if (node == null) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
long nodeKey = nodePos.asLong();
|
||||||
|
CompoundTag nodeTag = node.getPersistentData();
|
||||||
|
double live = keyInput(nodeKey, nodeTag, tag.getString("Key")) + keyInput(nodeKey, nodeTag, tag.getString("Key2"));
|
||||||
|
live = Math.max(-1.0, Math.min(1.0, live));
|
||||||
|
return Math.round(live / STEP) * STEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Like Futurism reads a key: pitch, yaw or roll with its sign, anything else (Throttle, Null) adds nothing. */
|
||||||
|
private static double keyInput(long nodeKey, CompoundTag nodeTag, String key) {
|
||||||
|
if (key.startsWith("Pitch") || key.startsWith("Yaw") || key.startsWith("Roll")) {
|
||||||
|
return LiveInputs.key(nodeKey, nodeTag, key);
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -216,6 +216,18 @@
|
|||||||
{
|
{
|
||||||
"id": "valkyrien_warium:green_control_surface_offset_top",
|
"id": "valkyrien_warium:green_control_surface_offset_top",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "warframes:airframe_control_surface",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "warframes:airframe_half_control_surface",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "warframes:airframe_stepped_control_surface",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"compatibilityLevel": "JAVA_17",
|
"compatibilityLevel": "JAVA_17",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
"futurism.ForceInducedShipsMixin",
|
"futurism.ForceInducedShipsMixin",
|
||||||
|
"futurism.VectoredAfterburnerSystemProcedureMixin",
|
||||||
"gecko.GeoModelMixin",
|
"gecko.GeoModelMixin",
|
||||||
"warium.AirBrakeTickProcedureMixin",
|
"warium.AirBrakeTickProcedureMixin",
|
||||||
"warium.BypassedCompressorScriptProcedureMixin",
|
"warium.BypassedCompressorScriptProcedureMixin",
|
||||||
|
|||||||
Reference in new issue
Block a user