Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80bb63d44f | ||
|
|
3478a04885 | ||
|
|
23f0f91ede |
No files matched your search
@@ -84,6 +84,10 @@ dependencies {
|
||||
compileOnly fg.deobf("blank:WariumVS:1.1.7")
|
||||
compileOnly fg.deobf("blank:futurism:0.0.4")
|
||||
|
||||
// GeckoLib 4.8.4, which Warium VS animates its control surfaces with (optional dependency, remapped, the surface animation mixin
|
||||
// only applies when it and Warium VS are present)
|
||||
compileOnly fg.deobf("maven.modrinth:geckolib:aC5KMoNg")
|
||||
|
||||
// AzureLib 3.2.7 for the animated block models (optional dependency, the common jar uses obfuscated names, so it is remapped)
|
||||
compileOnly fg.deobf("mod.azure.azurelib:azurelib-common-${minecraft_version}:${azurelib_version}")
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ mapping_version=2023.09.03-1.20.1
|
||||
mod_id=improved_aerodynamics
|
||||
mod_name=VS: Improved Aerodynamics
|
||||
mod_license=MIT
|
||||
mod_version=0.4.0
|
||||
mod_version=0.4.3
|
||||
azurelib_version=3.2.7
|
||||
mod_group_id=dev.lumareth.improvedaerodynamics
|
||||
mod_authors=Lumareth
|
||||
|
||||
@@ -204,6 +204,52 @@ public final class Config {
|
||||
.comment("The nozzle gives off light while its afterburner is lit")
|
||||
.define("lightEnabled", true);
|
||||
|
||||
static {
|
||||
BUILDER.pop();
|
||||
BUILDER.comment("Cursor aim: the mouse moves a cursor on screen, a virtual pilot flies the control node's craft by it").push("cursor_aim");
|
||||
}
|
||||
|
||||
public static final ForgeConfigSpec.BooleanValue CURSOR_AIM_ENABLED = BUILDER.comment("Whether pilots may use cursor aim on this server")
|
||||
.define("enabled", true);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_FULL_DEFLECTION_DEGREES = BUILDER.comment("Angle of the cursor from the gun reticle at which the virtual pilot turns as hard as it can: the maximum bank",
|
||||
"sideways, the full pitch rate up or down. Closer than that it turns more gently, so the reticle settles onto",
|
||||
"the cursor. How fast the craft can turn is worked out from the craft itself")
|
||||
.defineInRange("fullDeflectionDegrees", 30.0, 5.0, 90.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_DEADZONE_DEGREES = BUILDER.comment("Radius around the gun reticle in degrees inside which the pilot stops steering towards the cursor. The reticle",
|
||||
"then settles this far from the cursor. Outside it the command grows from zero, there is no jump at the edge")
|
||||
.defineInRange("deadzoneDegrees", 0.0, 0.0, 15.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MAX_ROLL_RATE_DEGREES = BUILDER.comment("Fastest roll in degrees per second the pilot asks for when it rolls to a bank angle")
|
||||
.defineInRange("maxRollRateDegrees", 150.0, 10.0, 720.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MAX_BANK_DEGREES = BUILDER.comment("Bank angle the pilot rolls to when the cursor is at full sideways deflection, at most. Craft whose wings",
|
||||
"cannot hold that bank in a level turn get less")
|
||||
.defineInRange("maxBankDegrees", 70.0, 10.0, 90.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_BANK_GAIN = BUILDER.comment("Roll rate in rad/s per radian of bank angle error, how eagerly the pilot rolls to the wanted bank and levels the wings")
|
||||
.defineInRange("bankGain", 3.0, 0.2, 20.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_RATE_GAIN = BUILDER.comment("How hard the pilot corrects its turn rate, in angular acceleration per rad/s of rate error. Lower is smoother,",
|
||||
"higher follows the cursor tighter and can start to oscillate")
|
||||
.defineInRange("rateGain", 5.0, 0.5, 40.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_PUSH_FACTOR = BUILDER.comment("A cursor below the gun reticle pushes the nose down. This is how much of the pull rate that may be, since",
|
||||
"negative g is harder on the craft and the pilot")
|
||||
.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")
|
||||
.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 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);
|
||||
|
||||
static {
|
||||
BUILDER.pop();
|
||||
}
|
||||
@@ -220,6 +266,45 @@ public final class Config {
|
||||
.comment("Vectored afterburner nozzle: afterburner ignition and heavy afterburner sound")
|
||||
.define("soundEnabled", true);
|
||||
|
||||
public static final ForgeConfigSpec.BooleanValue ANIMATE_SURFACES = CLIENT_BUILDER
|
||||
.comment("Control surfaces and flaps visibly deflect by as much as the flight physics deflects them, instead of Warium's",
|
||||
"fixed 20 degree steps. Needs physicsRateAerodynamics on the server")
|
||||
.define("animateControlSurfaces", true);
|
||||
|
||||
static {
|
||||
CLIENT_BUILDER.comment("Cursor aim, client side").push("cursor_aim");
|
||||
}
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_LOOK_AHEAD = CLIENT_BUILDER.comment("How strongly the camera looks into the direction of the turn. 0 never moves it, 1 (100%) turns it quickly",
|
||||
"to look where the cursor points. In between it follows more slowly")
|
||||
.defineInRange("lookAhead", 0.0, 0.0, 1.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MOUSE_SENSITIVITY = CLIENT_BUILDER.comment("Multiplier for how far the mouse moves the cursor (or the camera while the free look key is held)")
|
||||
.defineInRange("mouseSensitivity", 1.0, 0.05, 10.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_SPRING_BACK_SECONDS = CLIENT_BUILDER.comment("The cursor drifts to the gun reticle with this time constant in seconds, which ends the turn by itself.",
|
||||
"0, the default, keeps it where you put it in the world, so the craft turns until the reticle is on it")
|
||||
.defineInRange("springBackSeconds", 0.0, 0.0, 10.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MAX_ANGLE_DEGREES = CLIENT_BUILDER.comment("Furthest the cursor can go from the gun reticle, 135 is a 270 degree cone")
|
||||
.defineInRange("maxCursorAngleDegrees", 135.0, 10.0, 180.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_ZOOM_STEP = CLIENT_BUILDER.comment("Blocks the camera moves per mouse wheel notch")
|
||||
.defineInRange("zoomStep", 2.0, 0.1, 20.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_ZOOM_MAX = CLIENT_BUILDER.comment("Furthest the camera can be pulled back from its normal distance, in blocks")
|
||||
.defineInRange("zoomMax", 80.0, 0.0, 500.0);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue CURSOR_AIM_MARKER_SIZE = CLIENT_BUILDER.comment("Radius of the cursor ring on screen")
|
||||
.defineInRange("markerSize", 0.04, 0.01, 0.3);
|
||||
|
||||
public static final ForgeConfigSpec.BooleanValue CURSOR_AIM_SHOW_RETICLE = CLIENT_BUILDER.comment("Draw a small cross where the gun reticle points, the zero of the cursor, so you can see it when the camera looks away")
|
||||
.define("showReticle", true);
|
||||
|
||||
static {
|
||||
CLIENT_BUILDER.pop();
|
||||
}
|
||||
|
||||
static final ForgeConfigSpec CLIENT_SPEC = CLIENT_BUILDER.build();
|
||||
|
||||
private Config() {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package dev.lumareth.improvedAerodynamics;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.lumareth.improvedAerodynamics.client.aim.CursorAimClientConfig;
|
||||
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
|
||||
import dev.lumareth.improvedAerodynamics.exhaust.ExhaustSync;
|
||||
import dev.lumareth.improvedAerodynamics.exhaust.NozzleContent;
|
||||
import dev.lumareth.improvedAerodynamics.flight.CursorAimConfig;
|
||||
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
||||
@@ -29,12 +27,11 @@ public class ImprovedAerodynamics {
|
||||
MinecraftForge.EVENT_BUS.register(ExhaustSync.class);
|
||||
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.ControlMode.class);
|
||||
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.FlapInteraction.class);
|
||||
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.DeflectionSync.class);
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
|
||||
// Own file name: the default one is taken by Config.CLIENT_SPEC
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC, "improved_aerodynamics-pipper-client.toml");
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, CursorAimConfig.SPEC, "improved_aerodynamics-cursor-aim.toml");
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, CursorAimClientConfig.SPEC, "improved_aerodynamics-cursor-aim-client.toml");
|
||||
NozzleContent.register(FMLJavaModLoadingContext.get().getModEventBus());
|
||||
Network.register();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.Config;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
||||
@@ -32,6 +33,7 @@ import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.primitives.AABBdc;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3f;
|
||||
@@ -44,9 +46,10 @@ import java.lang.reflect.Method;
|
||||
/**
|
||||
* Cursor aim, the client side. While it is on:
|
||||
* <ul>
|
||||
* <li>the mouse moves a cursor instead of the camera. The cursor is a direction relative to the gun reticle (the
|
||||
* craft's boresight), at most {@code maxCursorAngleDegrees} away from it, and springs back to it when the mouse
|
||||
* rests. It is sent to the server every tick, where a virtual pilot flies the craft by it;</li>
|
||||
* <li>the mouse moves a cursor instead of the camera. The cursor is a fixed direction in the world: the craft
|
||||
* turning does not drag it along, so the virtual pilot on the server steers until the gun reticle (the craft's
|
||||
* boresight) sits on the cursor again. It is kept at most {@code maxCursorAngleDegrees} from the reticle and, if
|
||||
* spring back is set, drifts back to it. Every tick it is sent to the server as an offset from the reticle;</li>
|
||||
* <li>the camera is a chase view, looking along the reticle, or, with look ahead, towards the cursor;</li>
|
||||
* <li>holding the free look key moves the camera instead of the cursor, it eases back when released;</li>
|
||||
* <li>the mouse wheel changes how far the camera is behind the craft.</li>
|
||||
@@ -62,16 +65,23 @@ public final class CursorAimClient {
|
||||
private static final float MARKER_DISTANCE = 2.0F;
|
||||
private static final double LOOK_AHEAD_RATE = 6.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;
|
||||
// Chosen with /aero controls, told by the server
|
||||
private static boolean cursorAimMode;
|
||||
// Cursor as components along the craft's forward (gun reticle), right and up axes
|
||||
// Cursor as a world direction, null until the first frame puts it on the reticle
|
||||
private static Vector3d cursorWorld;
|
||||
// The same cursor as components along the craft's forward (gun reticle), right and up axes, as of the last update
|
||||
private static double cf = 1.0;
|
||||
private static double cr;
|
||||
private static double cu;
|
||||
// Camera direction in ship space and the free look offset on top of it, degrees
|
||||
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 offsetPitch;
|
||||
// Rotation the player was given last frame, to see what the mouse did since
|
||||
@@ -109,9 +119,91 @@ public final class CursorAimClient {
|
||||
deactivate(minecraft);
|
||||
return;
|
||||
}
|
||||
// The craft has turned since the last frame: the cursor stays put in the world, its offset changes
|
||||
Entity seat = AimSeat.seatOf(player);
|
||||
AimBasis basis = minecraft.level != null ? AimBasis.of(minecraft.level, seat.blockPosition()) : null;
|
||||
Quaterniondc rotation = basis != null ? shipRotation(minecraft.level, seat, false) : null;
|
||||
if (rotation != null) {
|
||||
cursorFromWorld(basis, rotation);
|
||||
}
|
||||
updateAirborne(minecraft.level, seat);
|
||||
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. */
|
||||
private static Quaterniondc shipRotation(ClientLevel level, Entity seat, boolean render) {
|
||||
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
||||
if (ship == null) {
|
||||
return null;
|
||||
}
|
||||
return render && ship instanceof ClientShip clientShip
|
||||
? clientShip.getRenderTransform().getShipToWorldRotation() : ship.getTransform().getShipToWorldRotation();
|
||||
}
|
||||
|
||||
/** Sets the cursor's components from its world direction, putting it on the reticle the first time. */
|
||||
private static void cursorFromWorld(AimBasis basis, Quaterniondc rotation) {
|
||||
if (cursorWorld == null) {
|
||||
double[] forward = basis.forward();
|
||||
cursorWorld = rotation.transform(new Vector3d(forward[0], forward[1], forward[2]));
|
||||
}
|
||||
Vector3d ship = rotation.transformInverse(cursorWorld, new Vector3d());
|
||||
double[] f = basis.forward();
|
||||
double[] r = basis.right();
|
||||
double[] u = basis.up();
|
||||
cf = ship.x * f[0] + ship.y * f[1] + ship.z * f[2];
|
||||
cr = ship.x * r[0] + ship.y * r[1] + ship.z * r[2];
|
||||
cu = ship.x * u[0] + ship.y * u[1] + ship.z * u[2];
|
||||
normalizeCursor();
|
||||
limitCursor();
|
||||
}
|
||||
|
||||
/** Stores the cursor's components back as its world direction. */
|
||||
private static void cursorToWorld(AimBasis basis, Quaterniondc rotation) {
|
||||
double[] ship = basis.toShip(cf, cr, cu);
|
||||
cursorWorld = rotation.transform(new Vector3d(ship[0], ship[1], ship[2]));
|
||||
}
|
||||
|
||||
/** Called when the server tells the client how its player flies. */
|
||||
public static void setMode(boolean cursorAim) {
|
||||
cursorAimMode = cursorAim;
|
||||
@@ -135,10 +227,12 @@ public final class CursorAimClient {
|
||||
return;
|
||||
}
|
||||
active = true;
|
||||
cursorWorld = null;
|
||||
cf = 1.0;
|
||||
cr = 0.0;
|
||||
cu = 0.0;
|
||||
cameraDirection = null;
|
||||
airborneTicks = 0;
|
||||
offsetYaw = 0.0;
|
||||
offsetPitch = 0.0;
|
||||
haveLast = false;
|
||||
@@ -184,7 +278,8 @@ public final class CursorAimClient {
|
||||
}
|
||||
Entity seat = AimSeat.seatOf(player);
|
||||
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
||||
if (basis == null) {
|
||||
Quaterniondc rotation = basis != null ? shipRotation(level, seat, true) : null;
|
||||
if (basis == null || rotation == null) {
|
||||
return;
|
||||
}
|
||||
if (minecraft.options.getCameraType() != CameraType.THIRD_PERSON_BACK) {
|
||||
@@ -207,10 +302,11 @@ public final class CursorAimClient {
|
||||
deltaPitch = 0.0;
|
||||
}
|
||||
}
|
||||
double sensitivity = CursorAimClientConfig.MOUSE_SENSITIVITY.get();
|
||||
double sensitivity = Config.CURSOR_AIM_MOUSE_SENSITIVITY.get();
|
||||
boolean freeLook = minecraft.screen == null && AimKeys.FREE_LOOK.isDown();
|
||||
|
||||
double spring = CursorAimClientConfig.SPRING_BACK_SECONDS.get();
|
||||
cursorFromWorld(basis, rotation);
|
||||
double spring = Config.CURSOR_AIM_SPRING_BACK_SECONDS.get();
|
||||
if (spring > 0.0) {
|
||||
shrinkCursor(Math.exp(-dt / spring));
|
||||
}
|
||||
@@ -223,14 +319,16 @@ public final class CursorAimClient {
|
||||
offsetYaw *= decay;
|
||||
offsetPitch *= decay;
|
||||
}
|
||||
cursorToWorld(basis, rotation);
|
||||
|
||||
// Camera: along the reticle, or turned towards the cursor by the look ahead setting
|
||||
double lookAhead = CursorAimClientConfig.LOOK_AHEAD.get();
|
||||
// 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[] forward = basis.forward();
|
||||
if (cameraDirection == null || lookAhead <= 0.0) {
|
||||
cameraDirection = forward.clone();
|
||||
} 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));
|
||||
}
|
||||
double[] direction = rotate(cameraDirection, basis.up(), -Math.toRadians(offsetYaw));
|
||||
@@ -295,7 +393,7 @@ public final class CursorAimClient {
|
||||
}
|
||||
|
||||
private static void limitCursor() {
|
||||
double limit = Math.toRadians(CursorAimClientConfig.MAX_CURSOR_ANGLE_DEGREES.get());
|
||||
double limit = Math.toRadians(Config.CURSOR_AIM_MAX_ANGLE_DEGREES.get());
|
||||
double angle = Math.acos(Mth.clamp(cf, -1.0, 1.0));
|
||||
if (angle <= limit) {
|
||||
return;
|
||||
@@ -318,7 +416,7 @@ public final class CursorAimClient {
|
||||
if (!active || minecraft.screen != null) {
|
||||
return;
|
||||
}
|
||||
zoom = Mth.clamp(zoom - event.getScrollDelta() * CursorAimClientConfig.ZOOM_STEP.get(), -3.0, CursorAimClientConfig.ZOOM_MAX.get());
|
||||
zoom = Mth.clamp(zoom - event.getScrollDelta() * Config.CURSOR_AIM_ZOOM_STEP.get(), -3.0, Config.CURSOR_AIM_ZOOM_MAX.get());
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
@@ -354,14 +452,10 @@ public final class CursorAimClient {
|
||||
}
|
||||
Entity seat = AimSeat.seatOf(player);
|
||||
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
||||
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
||||
if (basis == null || ship == null) {
|
||||
Quaterniondc rotation = basis != null ? shipRotation(level, seat, true) : null;
|
||||
if (rotation == null || cursorWorld == null) {
|
||||
return;
|
||||
}
|
||||
Quaterniondc rotation = ship instanceof ClientShip clientShip
|
||||
? clientShip.getRenderTransform().getShipToWorldRotation() : ship.getTransform().getShipToWorldRotation();
|
||||
double[] cursorShip = basis.toShip(cf, cr, cu);
|
||||
Vector3d cursorWorld = rotation.transform(new Vector3d(cursorShip[0], cursorShip[1], cursorShip[2]));
|
||||
double[] forward = basis.forward();
|
||||
Vector3d reticleWorld = rotation.transform(new Vector3d(forward[0], forward[1], forward[2]));
|
||||
|
||||
@@ -375,8 +469,8 @@ public final class CursorAimClient {
|
||||
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
||||
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
||||
|
||||
float size = (float) (CursorAimClientConfig.MARKER_SIZE.get() * MARKER_DISTANCE);
|
||||
if (CursorAimClientConfig.SHOW_RETICLE.get()) {
|
||||
float size = (float) (Config.CURSOR_AIM_MARKER_SIZE.get() * MARKER_DISTANCE);
|
||||
if (Config.CURSOR_AIM_SHOW_RETICLE.get()) {
|
||||
plus(builder, matrix, camera, reticleWorld, size * 0.35F, RETICLE_COLOR);
|
||||
}
|
||||
ring(builder, matrix, camera, cursorWorld, size, CURSOR_COLOR);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
/** Client side settings of cursor aim. Lives in improved_aerodynamics-cursor-aim-client.toml. */
|
||||
public final class CursorAimClientConfig {
|
||||
public static final ForgeConfigSpec SPEC;
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue LOOK_AHEAD;
|
||||
public static final ForgeConfigSpec.DoubleValue MOUSE_SENSITIVITY;
|
||||
public static final ForgeConfigSpec.DoubleValue SPRING_BACK_SECONDS;
|
||||
public static final ForgeConfigSpec.DoubleValue MAX_CURSOR_ANGLE_DEGREES;
|
||||
public static final ForgeConfigSpec.DoubleValue ZOOM_STEP;
|
||||
public static final ForgeConfigSpec.DoubleValue ZOOM_MAX;
|
||||
public static final ForgeConfigSpec.DoubleValue MARKER_SIZE;
|
||||
public static final ForgeConfigSpec.BooleanValue SHOW_RETICLE;
|
||||
|
||||
static {
|
||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||
builder.comment("Cursor aim, client side").push("cursor_aim");
|
||||
LOOK_AHEAD = builder.comment("How strongly the camera looks into the direction of the turn. 0 never moves it, 1 (100%) turns it quickly",
|
||||
"to look where the cursor points. In between it follows more slowly")
|
||||
.defineInRange("lookAhead", 0.0, 0.0, 1.0);
|
||||
MOUSE_SENSITIVITY = builder.comment("Multiplier for how far the mouse moves the cursor (or the camera while the free look key is held)")
|
||||
.defineInRange("mouseSensitivity", 1.0, 0.05, 10.0);
|
||||
SPRING_BACK_SECONDS = builder.comment("The cursor returns to the gun reticle with this time constant in seconds when you stop moving the mouse.",
|
||||
"0 makes it stay where you left it")
|
||||
.defineInRange("springBackSeconds", 1.0, 0.0, 10.0);
|
||||
MAX_CURSOR_ANGLE_DEGREES = builder.comment("Furthest the cursor can go from the gun reticle, 135 is a 270 degree cone")
|
||||
.defineInRange("maxCursorAngleDegrees", 135.0, 10.0, 180.0);
|
||||
ZOOM_STEP = builder.comment("Blocks the camera moves per mouse wheel notch")
|
||||
.defineInRange("zoomStep", 2.0, 0.1, 20.0);
|
||||
ZOOM_MAX = builder.comment("Furthest the camera can be pulled back from its normal distance, in blocks")
|
||||
.defineInRange("zoomMax", 80.0, 0.0, 500.0);
|
||||
MARKER_SIZE = builder.comment("Radius of the cursor ring on screen")
|
||||
.defineInRange("markerSize", 0.04, 0.01, 0.3);
|
||||
SHOW_RETICLE = builder.comment("Draw a small cross where the gun reticle points, the zero of the cursor, so you can see it when the camera looks away")
|
||||
.define("showReticle", true);
|
||||
builder.pop();
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
private CursorAimClientConfig() {
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package dev.lumareth.improvedAerodynamics.client.animation;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.Config;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import software.bernie.geckolib.core.animatable.model.CoreGeoBone;
|
||||
import software.bernie.geckolib.model.GeoModel;
|
||||
|
||||
/**
|
||||
* Client side of the control surface animation. The server tells the clients how far each control surface is deflected
|
||||
* (see {@code DeflectionSync}); this class keeps those values, eases towards them, and sets the rotation of the "Fin"
|
||||
* bone of Warium's control surface models to them after GeckoLib has played Warium's own fixed animation.
|
||||
* <p>
|
||||
* 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 and Y for rotations), and the same sign is used here, so a surface moves the
|
||||
* 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)
|
||||
public final class SurfaceAnimation {
|
||||
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
|
||||
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
|
||||
private static final double EASE_SECONDS = 0.07;
|
||||
|
||||
private static final class Entry {
|
||||
float target;
|
||||
float shown;
|
||||
long updated;
|
||||
long drawn;
|
||||
boolean placed;
|
||||
}
|
||||
|
||||
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() {
|
||||
}
|
||||
|
||||
/** New deflection of a control surface in degrees, in Warium's animation sign (positive is its "input up" tilt). */
|
||||
public static void set(long pos, float degrees) {
|
||||
Entry entry = ENTRIES.computeIfAbsent(pos, key -> new Entry());
|
||||
entry.target = degrees;
|
||||
entry.updated = System.nanoTime();
|
||||
if (!entry.placed) {
|
||||
// First value of a surface: show it right away, it may have been there before the player came near
|
||||
entry.shown = degrees;
|
||||
entry.placed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void apply(BlockEntity blockEntity, GeoModel<?> model) {
|
||||
if (!Config.ANIMATE_SURFACES.get()) {
|
||||
return;
|
||||
}
|
||||
if (blockEntity.getClass().getName().equals(FUTURISM_VECTORED)) {
|
||||
applyVectored(blockEntity, model);
|
||||
return;
|
||||
}
|
||||
if (ENTRIES.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Entry entry = ENTRIES.get(blockEntity.getBlockPos().asLong());
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
long now = System.nanoTime();
|
||||
if (now - entry.updated > STALE_NANOS) {
|
||||
ENTRIES.remove(blockEntity.getBlockPos().asLong());
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
double dt = (now - entry.drawn) / 1.0E9;
|
||||
entry.shown += (entry.target - entry.shown) * (float) (1.0 - Math.exp(-dt / EASE_SECONDS));
|
||||
}
|
||||
entry.drawn = now;
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) {
|
||||
ENTRIES.clear();
|
||||
VECTORED.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
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.nbt.CompoundTag;
|
||||
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
|
||||
* 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
|
||||
* 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 {
|
||||
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 key2 = own.getString("Key2");
|
||||
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);
|
||||
double roll = clamp(tag.getDouble("Roll")) * sign(key, key2, "Roll", 0.0);
|
||||
double yaw = clamp(tag.getDouble("Yaw")) * sign(key, key2, "Yaw", 1.0);
|
||||
long nodeKey = nodePos.asLong();
|
||||
double pitch = clamp(LiveInputs.value(nodeKey, tag, ControlAxis.PITCH)) * sign(key, key2, "Pitch", unset ? 1.0 : 0.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));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
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;
|
||||
// Time constant of the smoothing of measured control rates
|
||||
private static final double RATE_SMOOTHING_SECONDS = 0.3;
|
||||
private static final ControlAxis[] MEASURED_AXES = {ControlAxis.PITCH, ControlAxis.YAW, ControlAxis.ROLL};
|
||||
|
||||
private final SubstepClock clock;
|
||||
@@ -41,7 +43,12 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
private Frame current;
|
||||
private boolean[] stalled = new boolean[0];
|
||||
private boolean wingStalled;
|
||||
// Lift the wings could make at the edge of the stall in the airflow of the last substep, NaN without the improved
|
||||
// flight model, which has no stall
|
||||
private double maxLift = Double.NaN;
|
||||
private final Map<Long, NodeState> nodeStates = new ConcurrentHashMap<>();
|
||||
// Every ship's node states by node position, for the control surface animation on the server thread
|
||||
private static final Map<Long, NodeState> ALL_NODE_STATES = new ConcurrentHashMap<>();
|
||||
private final Wrench wrench = new Wrench();
|
||||
private final Wrench probe = new Wrench();
|
||||
|
||||
@@ -57,6 +64,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double trimOutput;
|
||||
// Deflection of this node's flaps in radians, moves towards what the flap lever and airspeed allow
|
||||
double flapAngle;
|
||||
// Wall clock time of the last physics substep that ran this node, to tell whether its ship is flying
|
||||
volatile long updatedMillis;
|
||||
// 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
|
||||
@@ -67,6 +76,17 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
final Vector3d yawResponseBuilding = new Vector3d();
|
||||
final Vector3d rollResponse = new Vector3d();
|
||||
final Vector3d rollResponseBuilding = new Vector3d();
|
||||
// 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
|
||||
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();
|
||||
volatile double savedTrim;
|
||||
|
||||
@@ -151,9 +171,14 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double dt = clock.substepSeconds();
|
||||
boolean improved = Config.IMPROVED_FLIGHT_MODEL.get();
|
||||
Matrix3dc inertia = ship.getMomentOfInertia();
|
||||
// Same frames and scaling as Warium applies its forces in
|
||||
double scale = transform.getShipToWorldScaling().x();
|
||||
double forceScale = autoScale ? scale * scale * scale : 1.0;
|
||||
double liftAcceleration = maxLift * forceScale / ship.getMass();
|
||||
|
||||
updateNodes(frame, worldVelocity, transform.getPositionInWorld().y(), rotation, inertia, w, dt, improved);
|
||||
updateNodes(frame, worldVelocity, transform.getPositionInWorld().y(), rotation, inertia, w, dt, improved, liftAcceleration);
|
||||
evaluate(frame, centerOfMass, v, w, improved, true, wrench);
|
||||
measureControlRates(frame, centerOfMass, v, w, improved, dt);
|
||||
|
||||
if (Config.IMPLICIT_DAMPING.get()) {
|
||||
// Implicit Euler for the damping part: scale each axis by 1 / (1 + dt * damping / inertia), using the
|
||||
@@ -180,9 +205,6 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
}
|
||||
}
|
||||
|
||||
// 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()) {
|
||||
@@ -209,9 +231,15 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
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));
|
||||
nodeStates.keySet().removeIf(key -> {
|
||||
boolean gone = !frame.nodes.containsKey((long) key);
|
||||
if (gone) {
|
||||
ALL_NODE_STATES.remove(key);
|
||||
}
|
||||
return gone;
|
||||
});
|
||||
for (NodeData node : frame.nodes.values()) {
|
||||
nodeStates.computeIfAbsent(node.key(), key -> new NodeState(node));
|
||||
ALL_NODE_STATES.put(node.key(), nodeStates.computeIfAbsent(node.key(), key -> new NodeState(node)));
|
||||
}
|
||||
current = frame;
|
||||
}
|
||||
@@ -229,6 +257,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double[] r = new double[3];
|
||||
if (commit) {
|
||||
wingStalled = false;
|
||||
maxLift = improved ? 0.0 : Double.NaN;
|
||||
for (NodeState state : nodeStates.values()) {
|
||||
state.pitchResponseBuilding.zero();
|
||||
state.yawResponseBuilding.zero();
|
||||
@@ -251,6 +280,9 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
if (commit) {
|
||||
stalled[i] = nowStalled;
|
||||
wingStalled |= nowStalled && s.type() == SurfaceType.WING;
|
||||
if (s.type() == SurfaceType.WING || s.type() == SurfaceType.CONTROL_SURFACE && s.wingGroup()) {
|
||||
maxLift += ImprovedAero.maxLift(s, v, w, r, flap, weight, tuning);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
WariumAero.evaluate(s, v, w, r, input, weight, out);
|
||||
@@ -296,6 +328,37 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For every node cursor aim flies: the steady pitch rate its pitch surfaces can hold, full deflection torque over
|
||||
* the craft's pitch damping, both measured about the axis the pitch surfaces turn the craft about.
|
||||
*/
|
||||
private void measureControlRates(Frame frame, Vector3dc centerOfMass, double[] v, double[] w, boolean improved, double dt) {
|
||||
for (NodeData node : frame.nodes.values()) {
|
||||
NodeState state = nodeStates.get(node.key());
|
||||
if (state == null || !CursorAimRegistry.isActive(node.key())) {
|
||||
continue;
|
||||
}
|
||||
double response = state.pitchResponse.length();
|
||||
if (response < 1.0E-9) {
|
||||
continue;
|
||||
}
|
||||
Vector3d axis = state.pitchResponse.div(response, new Vector3d());
|
||||
double[] turned = {w[0] + ANGULAR_EPSILON * axis.x, w[1] + ANGULAR_EPSILON * axis.y, w[2] + ANGULAR_EPSILON * axis.z};
|
||||
evaluate(frame, centerOfMass, v, turned, improved, false, probe);
|
||||
double damping = 0.0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
damping -= (probe.totalTorque(i) - wrench.totalTorque(i)) * axis.get(i);
|
||||
}
|
||||
damping /= ANGULAR_EPSILON;
|
||||
if (!(damping > 1.0E-9)) {
|
||||
continue;
|
||||
}
|
||||
double rate = response / damping;
|
||||
state.controlRate = Double.isNaN(state.controlRate) ? rate
|
||||
: state.controlRate + (rate - state.controlRate) * Math.min(1.0, dt / RATE_SMOOTHING_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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, ControlAxis nudged, double extra) {
|
||||
if (node == null || state == null) {
|
||||
@@ -318,7 +381,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
}
|
||||
|
||||
private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia,
|
||||
double[] w, double dt, boolean improved) {
|
||||
double[] w, double dt, boolean improved, double liftAcceleration) {
|
||||
double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY;
|
||||
// Torque of the last substep, in ship axes, which the flight computer separates from its own surfaces' share
|
||||
double[] torque = {wrench.totalTorque(0), wrench.totalTorque(1), wrench.totalTorque(2)};
|
||||
@@ -328,11 +391,13 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
continue;
|
||||
}
|
||||
// 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, dt);
|
||||
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);
|
||||
state.updatedMillis = System.currentTimeMillis();
|
||||
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, torque, dt, improved && wingStalled);
|
||||
}
|
||||
}
|
||||
@@ -351,6 +416,11 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
state.flapAngle = moveTowards(state.flapAngle, target, Math.toRadians(Config.FLAP_SLEW_DEGREES.get()) * dt);
|
||||
}
|
||||
|
||||
/** The physics state of a control node, null if no physics runs for it. */
|
||||
static NodeState nodeState(long nodeKey) {
|
||||
return ALL_NODE_STATES.get(nodeKey);
|
||||
}
|
||||
|
||||
static double moveTowards(double value, double target, double maxStep) {
|
||||
if (Math.abs(target - value) <= maxStep) {
|
||||
return target;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
/** Server side settings of the cursor aim virtual pilot. Lives in improved_aerodynamics-cursor-aim.toml. */
|
||||
public final class CursorAimConfig {
|
||||
public static final ForgeConfigSpec SPEC;
|
||||
|
||||
public static final ForgeConfigSpec.BooleanValue ENABLED;
|
||||
public static final ForgeConfigSpec.DoubleValue FULL_DEFLECTION_DEGREES;
|
||||
public static final ForgeConfigSpec.DoubleValue DEADZONE_DEGREES;
|
||||
public static final ForgeConfigSpec.DoubleValue MAX_TURN_RATE_DEGREES;
|
||||
public static final ForgeConfigSpec.DoubleValue MAX_ROLL_RATE_DEGREES;
|
||||
public static final ForgeConfigSpec.DoubleValue MAX_BANK_DEGREES;
|
||||
public static final ForgeConfigSpec.DoubleValue BANK_GAIN;
|
||||
public static final ForgeConfigSpec.DoubleValue RATE_GAIN;
|
||||
public static final ForgeConfigSpec.DoubleValue PUSH_FACTOR;
|
||||
public static final ForgeConfigSpec.DoubleValue STALLED_PITCH_FACTOR;
|
||||
public static final ForgeConfigSpec.DoubleValue MIN_SPEED;
|
||||
|
||||
static {
|
||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||
builder.comment("Cursor aim: the mouse moves a cursor on screen, a virtual pilot flies the control node's craft by it").push("cursor_aim");
|
||||
ENABLED = builder.comment("Whether pilots may use cursor aim on this server")
|
||||
.define("enabled", true);
|
||||
FULL_DEFLECTION_DEGREES = builder.comment("Angle of the cursor from the gun reticle at which the virtual pilot asks for full deflection: the maximum bank",
|
||||
"sideways, the full pitch rate up or down")
|
||||
.defineInRange("fullDeflectionDegrees", 30.0, 5.0, 90.0);
|
||||
DEADZONE_DEGREES = builder.comment("Radius around the gun reticle in degrees inside which the cursor commands nothing, so the craft flies straight.",
|
||||
"Outside it the command grows from zero, there is no jump at the edge")
|
||||
.defineInRange("deadzoneDegrees", 3.0, 0.0, 15.0);
|
||||
MAX_TURN_RATE_DEGREES = builder.comment("Turn rate in degrees per second the pilot asks for at full deflection, both for the turn at maximum bank and for pitching")
|
||||
.defineInRange("maxTurnRateDegrees", 40.0, 5.0, 360.0);
|
||||
MAX_ROLL_RATE_DEGREES = builder.comment("Fastest roll in degrees per second the pilot asks for when it rolls to a bank angle")
|
||||
.defineInRange("maxRollRateDegrees", 150.0, 10.0, 720.0);
|
||||
MAX_BANK_DEGREES = builder.comment("Bank angle the pilot rolls to when the cursor is at full sideways deflection")
|
||||
.defineInRange("maxBankDegrees", 70.0, 10.0, 90.0);
|
||||
BANK_GAIN = builder.comment("Roll rate in rad/s per radian of bank angle error, how eagerly the pilot rolls to the wanted bank and levels the wings")
|
||||
.defineInRange("bankGain", 3.0, 0.2, 20.0);
|
||||
RATE_GAIN = builder.comment("How hard the pilot corrects its turn rate, in angular acceleration per rad/s of rate error. Lower is smoother,",
|
||||
"higher follows the cursor tighter and can start to oscillate")
|
||||
.defineInRange("rateGain", 5.0, 0.5, 40.0);
|
||||
PUSH_FACTOR = builder.comment("A cursor below the gun reticle pushes the nose down. This is how much of the pull rate that may be, since",
|
||||
"negative g is harder on the craft and the pilot")
|
||||
.defineInRange("pushFactor", 0.4, 0.0, 1.0);
|
||||
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);
|
||||
MIN_SPEED = builder.comment("Below this speed in blocks per second the pilot does nothing, the surfaces have no effect anyway")
|
||||
.defineInRange("minSpeed", 15.0, 0.0, 200.0);
|
||||
builder.pop();
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
private CursorAimConfig() {
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.Config;
|
||||
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
@@ -24,7 +25,7 @@ public final class CursorAimServer {
|
||||
}
|
||||
long nodeKey = basis.nodePos().asLong();
|
||||
double length = Math.sqrt(cf * cf + cr * cr + cu * cu);
|
||||
if (!active || !CursorAimConfig.ENABLED.get() || !(length > 1.0E-6) || !Double.isFinite(length)) {
|
||||
if (!active || !Config.CURSOR_AIM_ENABLED.get() || !(length > 1.0E-6) || !Double.isFinite(length)) {
|
||||
CursorAimRegistry.clear(nodeKey);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,54 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.Config;
|
||||
import org.joml.Matrix3d;
|
||||
import org.joml.Matrix3dc;
|
||||
import org.joml.Quaterniondc;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
/**
|
||||
* The virtual pilot of cursor aim. The cursor is a stick that springs back to the gun reticle:
|
||||
* The virtual pilot of cursor aim. The cursor is a direction fixed in the world, and the pilot turns the craft until
|
||||
* the gun reticle sits on it, so the offset it gets shrinks to nothing as the turn completes:
|
||||
* <ul>
|
||||
* <li>sideways it commands a bank angle and turn: the further out, the steeper the bank and the harder the pull
|
||||
* that comes with it. Centered it levels the wings, so a centered cursor is straight flight;</li>
|
||||
* <li>up and down it commands a pitch rate;</li>
|
||||
* <li>close to the reticle it steers with yaw and pitch directly instead of banking, so aiming does not rock the
|
||||
* wings.</li>
|
||||
* <li>it banks towards the cursor, judged with the wings level so the bank does not hide where the cursor is: the
|
||||
* further the cursor is to the side, the steeper the bank. While banked it pulls as much as a level turn at that
|
||||
* 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
|
||||
* 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
|
||||
* aiming does not rock the wings.</li>
|
||||
* </ul>
|
||||
* The wanted rotation is turned into stick inputs by the measured effect of each input on the craft (torque per unit
|
||||
* of input, see {@link AeroState}), so it works for any craft however its surfaces are mixed or oriented: angular
|
||||
* acceleration wanted times inertia, solved for the three inputs with a little regularization for axes that have no
|
||||
* surfaces. Keys the pilot holds win over it, per axis.
|
||||
* <p>
|
||||
* How hard the craft can turn is worked out live, not configured: the lift its wings can make at the edge of the stall
|
||||
* at the current speed and altitude, over its mass and speed, and the steady pitch rate its pitch surfaces can hold
|
||||
* against its pitch damping, whichever is lower. The same lift limit caps the bank to what the craft can hold level, so
|
||||
* a heavy bomber banks gently and turns wide while a light fighter racks into a steep, tight turn.
|
||||
*/
|
||||
final class CursorPilot {
|
||||
private static final double FINE_START = Math.toRadians(4.0);
|
||||
private static final double FINE_END = Math.toRadians(16.0);
|
||||
private static final double MANUAL_OVERRIDE = 0.05;
|
||||
private static final double MAX_PULL_BANK = Math.toRadians(90.0);
|
||||
// Gravity of VS
|
||||
private static final double GRAVITY = 10.0;
|
||||
// Share of the stall limited lift the pilot plans with, so it does not ride the stall
|
||||
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
|
||||
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
|
||||
private static final double FALLBACK_TURN_RATE = Math.toRadians(30.0);
|
||||
|
||||
private CursorPilot() {
|
||||
}
|
||||
@@ -32,57 +57,128 @@ 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
|
||||
*/
|
||||
static NodeData apply(AeroState.NodeState state, NodeData node, Quaterniondc rotation, Matrix3dc inertia, double[] w,
|
||||
double speed, boolean wingStalled) {
|
||||
if (!CursorAimConfig.ENABLED.get()) {
|
||||
double speed, boolean wingStalled, double liftAcceleration, double dt) {
|
||||
if (!Config.CURSOR_AIM_ENABLED.get()) {
|
||||
return node;
|
||||
}
|
||||
CursorAimRegistry.Aim aim = CursorAimRegistry.get(node.key());
|
||||
if (aim == null || speed < CursorAimConfig.MIN_SPEED.get()) {
|
||||
if (aim == null) {
|
||||
return node;
|
||||
}
|
||||
|
||||
double fullAngle = Math.toRadians(CursorAimConfig.FULL_DEFLECTION_DEGREES.get());
|
||||
double maxTurn = Math.toRadians(CursorAimConfig.MAX_TURN_RATE_DEGREES.get());
|
||||
double maxRoll = Math.toRadians(CursorAimConfig.MAX_ROLL_RATE_DEGREES.get());
|
||||
double maxBank = Math.toRadians(CursorAimConfig.MAX_BANK_DEGREES.get());
|
||||
double fullAngle = Math.toRadians(Config.CURSOR_AIM_FULL_DEFLECTION_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[] f = aim.forward();
|
||||
double[] r = aim.right();
|
||||
double[] u = aim.up();
|
||||
|
||||
// Cursor as angles from the reticle: sideways (positive right) and up, both through any 270 degree cone
|
||||
double sideways = Math.atan2(aim.cr(), aim.cf());
|
||||
double vertical = Math.atan2(aim.cu(), Math.hypot(aim.cf(), aim.cr()));
|
||||
// Deadzone around the reticle: shrink the cursor offset by its radius, direction kept
|
||||
double offset = Math.hypot(sideways, vertical);
|
||||
double deadzone = Math.toRadians(CursorAimConfig.DEADZONE_DEGREES.get());
|
||||
if (offset > 1.0E-9) {
|
||||
double scale = Math.max(0.0, offset - deadzone) / offset;
|
||||
sideways *= scale;
|
||||
vertical *= scale;
|
||||
}
|
||||
double lateral = saturate(sideways / fullAngle);
|
||||
double blend = smoothstep(Math.abs(sideways), FINE_START, FINE_END);
|
||||
learnSigns(state, f, r, u);
|
||||
|
||||
// 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 upWorld = rotation.transform(new Vector3d(u[0], u[1], u[2]));
|
||||
double bank = Math.atan2(-rightWorld.y, upWorld.y);
|
||||
|
||||
double bankCommand = maxBank * lateral * blend;
|
||||
double rollRate = clamp(CursorAimConfig.BANK_GAIN.get() * (bankCommand - bank), -maxRoll, maxRoll);
|
||||
|
||||
// Pitch: the stick, plus the pull that carries the turn while banked
|
||||
double pitchStick = saturate(vertical / fullAngle);
|
||||
double pitchRate = maxTurn * (pitchStick >= 0.0 ? pitchStick : pitchStick * CursorAimConfig.PUSH_FACTOR.get());
|
||||
if (Math.abs(bank) < MAX_PULL_BANK) {
|
||||
pitchRate += maxTurn * Math.abs(lateral) * blend * Math.sin(Math.abs(bank));
|
||||
// Deadzone around the reticle: shrink the cursor's offset by its radius, direction kept
|
||||
double cf = aim.cf();
|
||||
double cr = aim.cr();
|
||||
double cu = aim.cu();
|
||||
double deadzone = Math.toRadians(Config.CURSOR_AIM_DEADZONE_DEGREES.get());
|
||||
double offset = Math.acos(clamp(cf, -1.0, 1.0));
|
||||
double across = Math.hypot(cr, cu);
|
||||
if (deadzone > 0.0 && offset > 1.0E-9 && across > 1.0E-9) {
|
||||
double shrunk = Math.max(0.0, offset - deadzone);
|
||||
cf = Math.cos(shrunk);
|
||||
cr *= Math.sin(shrunk) / across;
|
||||
cu *= Math.sin(shrunk) / across;
|
||||
}
|
||||
if (wingStalled && pitchRate > 0.0) {
|
||||
pitchRate *= CursorAimConfig.STALLED_PITCH_FACTOR.get();
|
||||
// Cursor as angles from the reticle as seen from the cockpit: sideways (positive right) and up, through any
|
||||
// 270 degree cone
|
||||
double sideways = Math.atan2(cr, cf);
|
||||
double vertical = Math.atan2(cu, Math.hypot(cf, cr));
|
||||
// The cursor as it would be with the wings level: what the bank is chosen by
|
||||
double levelRight = cr * Math.cos(bank) + cu * Math.sin(bank);
|
||||
double levelUp = cu * Math.cos(bank) - cr * Math.sin(bank);
|
||||
double levelVertical = Math.atan2(levelUp, Math.hypot(cf, levelRight));
|
||||
|
||||
if (speed < Config.CURSOR_AIM_MIN_SPEED.get()) {
|
||||
// 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,
|
||||
// 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 push = Config.CURSOR_AIM_PUSH_FACTOR.get();
|
||||
// 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);
|
||||
if (cosBank > 0.0) {
|
||||
double sinBank = Math.sin(bank);
|
||||
pitchRate += GRAVITY * sinBank * sinBank / (speed * Math.max(cosBank, 0.1));
|
||||
}
|
||||
// Stalled wings ease the pull back, and it comes back as gently once they fly again: switching it at once with
|
||||
// the stall would make the craft buck in and out of the stall
|
||||
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
|
||||
double yawRate = maxTurn * lateral * (1.0 - blend);
|
||||
double yawRate = maxTurn * saturate(sideways / fullAngle) * (1.0 - blend);
|
||||
|
||||
// Wanted angular velocity in ship axes: roll about forward, pitch about right, yaw (nose right) about -up
|
||||
Vector3d wanted = new Vector3d(
|
||||
@@ -90,7 +186,7 @@ final class CursorPilot {
|
||||
rollRate * f[1] + pitchRate * r[1] - yawRate * u[1],
|
||||
rollRate * f[2] + pitchRate * r[2] - yawRate * u[2]);
|
||||
Vector3d error = wanted.sub(w[0], w[1], w[2]);
|
||||
Vector3d acceleration = error.mul(CursorAimConfig.RATE_GAIN.get());
|
||||
Vector3d acceleration = error.mul(Config.CURSOR_AIM_RATE_GAIN.get());
|
||||
Vector3d torque = inertia.transform(acceleration, new Vector3d());
|
||||
|
||||
double[] inputs = solve(state.pitchResponse, state.yawResponse, state.rollResponse, torque);
|
||||
@@ -101,6 +197,11 @@ final class CursorPilot {
|
||||
double pitch = clamp(inputs[0] - state.trimOutput, -1.0, 1.0);
|
||||
double yaw = clamp(inputs[1], -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(),
|
||||
Math.abs(node.pitch()) > MANUAL_OVERRIDE ? node.pitch() : pitch,
|
||||
Math.abs(node.yaw()) > MANUAL_OVERRIDE ? node.yaw() : yaw,
|
||||
@@ -108,6 +209,25 @@ final class CursorPilot {
|
||||
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. */
|
||||
private static double[] solve(Vector3d pitch, Vector3d yaw, Vector3d roll, Vector3d torque) {
|
||||
Matrix3d responses = new Matrix3d(pitch, yaw, roll);
|
||||
@@ -129,6 +249,11 @@ final class CursorPilot {
|
||||
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) {
|
||||
return clamp(value, -1.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.Config;
|
||||
import dev.lumareth.improvedAerodynamics.network.DeflectionPacket;
|
||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.server.ServerStoppedEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
import org.joml.Vector3d;
|
||||
import org.valkyrienskies.core.api.ships.Ship;
|
||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tells the clients near a ship how far its control surfaces and flaps are deflected, so they can show it, see
|
||||
* {@code SurfaceAnimation}. Every tick each Warium control surface block reports its deflection from the procedure that
|
||||
* runs it ({@link WariumCapture#begin}); at the end of the tick what changed goes out, in one packet per ship, and
|
||||
* everything again once a second so players who arrive later get it too.
|
||||
* <p>
|
||||
* The deflection is what the physics uses: the smoothed pilot input plus trim and flap angle of the surface's control
|
||||
* node. While the ship is not flying (no physics, nothing is moving) the node's raw input and flap lever are used, and the
|
||||
* flaps are moved towards their angle here, at the configured speed.
|
||||
*/
|
||||
public final class DeflectionSync {
|
||||
/** Degrees of deflection the physics gives a surface at full input, see ImprovedAero.MAX_DEFLECTION. */
|
||||
private static final double FULL_INPUT_DEGREES = 20.0;
|
||||
private static final long RESEND_TICKS = 20;
|
||||
private static final double RANGE = 256.0;
|
||||
|
||||
private record Batch(ServerLevel level, double x, double y, double z, LongArrayList positions, ShortArrayList tenths) {
|
||||
}
|
||||
|
||||
private static final Map<ResourceKey<Level>, Long2ShortOpenHashMap> LAST_VALUE = new HashMap<>();
|
||||
private static final Map<ResourceKey<Level>, Long2LongOpenHashMap> LAST_SENT = new HashMap<>();
|
||||
// Flap angle (radians) shown while a node is not flying, and the tick it was last moved
|
||||
private static final Map<Long, double[]> STATIC_FLAPS = new HashMap<>();
|
||||
private static final Map<Long, Batch> BATCHES = new HashMap<>();
|
||||
|
||||
private DeflectionSync() {
|
||||
}
|
||||
|
||||
/** Called by the procedure of every control surface block each tick, before it adds its forces. */
|
||||
static void report(ServerLevel level, BlockPos pos, SurfaceType type) {
|
||||
if (!type.controllable) {
|
||||
return;
|
||||
}
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity == null) {
|
||||
return;
|
||||
}
|
||||
CompoundTag tag = blockEntity.getPersistentData();
|
||||
BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ"));
|
||||
BlockEntity nodeEntity = level.getBlockEntity(nodePos);
|
||||
if (nodeEntity == null || !tag.contains("ControlX")) {
|
||||
return;
|
||||
}
|
||||
long nodeKey = nodePos.asLong();
|
||||
CompoundTag nodeTag = nodeEntity.getPersistentData();
|
||||
AeroState.NodeState state = LiveInputs.flying(nodeKey);
|
||||
boolean flying = state != null;
|
||||
|
||||
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));
|
||||
if (type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(WariumCapture.FLAP_TAG)) {
|
||||
degrees += Math.toDegrees(flying ? state.flapAngle : staticFlap(level, nodeKey, nodeTag));
|
||||
}
|
||||
|
||||
short tenths = (short) Math.round(Math.max(-90.0, Math.min(90.0, degrees)) * 10.0);
|
||||
ResourceKey<Level> dimension = level.dimension();
|
||||
Long2ShortOpenHashMap lastValue = LAST_VALUE.computeIfAbsent(dimension, key -> new Long2ShortOpenHashMap());
|
||||
Long2LongOpenHashMap lastSent = LAST_SENT.computeIfAbsent(dimension, key -> new Long2LongOpenHashMap());
|
||||
long posKey = pos.asLong();
|
||||
long now = level.getGameTime();
|
||||
if (lastSent.containsKey(posKey) && lastValue.get(posKey) == tenths && now - lastSent.get(posKey) < RESEND_TICKS) {
|
||||
return;
|
||||
}
|
||||
lastValue.put(posKey, tenths);
|
||||
lastSent.put(posKey, now);
|
||||
|
||||
Ship ship = VSGameUtilsKt.getShipManagingPos(level, pos);
|
||||
long shipId = ship != null ? ship.getId() : Long.MIN_VALUE;
|
||||
Batch batch = BATCHES.get(shipId);
|
||||
if (batch == null) {
|
||||
Vector3d world = new Vector3d(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
|
||||
if (ship != null) {
|
||||
ship.getShipToWorld().transformPosition(world);
|
||||
}
|
||||
batch = new Batch(level, world.x, world.y, world.z, new LongArrayList(), new ShortArrayList());
|
||||
BATCHES.put(shipId, batch);
|
||||
}
|
||||
batch.positions.add(posKey);
|
||||
batch.tenths.add(tenths);
|
||||
}
|
||||
|
||||
/** One key's share of a surface's input, like Warium reads "Pitch+" / "Roll-". Throttle keys do not deflect it. */
|
||||
private static double axisInput(long nodeKey, CompoundTag nodeTag, String key) {
|
||||
return ControlAxis.of(key) == ControlAxis.THROTTLE ? 0.0 : LiveInputs.key(nodeKey, nodeTag, key);
|
||||
}
|
||||
|
||||
/** Flap angle of a node that is not flying: moves towards the lever's angle, once per tick however many flaps ask. */
|
||||
private static double staticFlap(ServerLevel level, long nodeKey, CompoundTag nodeTag) {
|
||||
double target = Math.toRadians(FlapSetting.of(nodeTag.getInt(WariumCapture.FLAP_SETTING_TAG)).degrees());
|
||||
double[] entry = STATIC_FLAPS.computeIfAbsent(nodeKey, key -> new double[]{target, Double.NaN});
|
||||
long now = level.getGameTime();
|
||||
if (entry[1] != now) {
|
||||
entry[1] = now;
|
||||
entry[0] = AeroState.moveTowards(entry[0], target, Math.toRadians(Config.FLAP_SLEW_DEGREES.get()) / 20.0);
|
||||
}
|
||||
return entry[0];
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END || BATCHES.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Batch batch : BATCHES.values()) {
|
||||
DeflectionPacket packet = new DeflectionPacket(batch.positions.toLongArray(), batch.tenths.toShortArray());
|
||||
Network.CHANNEL.send(PacketDistributor.NEAR.with(() -> new PacketDistributor.TargetPoint(batch.x, batch.y, batch.z,
|
||||
RANGE, batch.level.dimension())), packet);
|
||||
}
|
||||
BATCHES.clear();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onServerStopped(ServerStoppedEvent event) {
|
||||
BATCHES.clear();
|
||||
LAST_VALUE.clear();
|
||||
LAST_SENT.clear();
|
||||
STATIC_FLAPS.clear();
|
||||
}
|
||||
}
|
||||
@@ -129,11 +129,8 @@ final class ImprovedAero {
|
||||
|
||||
// Low aspect ratio wings keep their flow attached longer thanks to leading-edge vortices. Control surfaces
|
||||
// in a wing's group stall with the wing instead of before it
|
||||
double vortexFactor = tuning.vortex() && (s.type() == SurfaceType.WING || s.wingGroup() && s.type().controllable)
|
||||
? VortexLift.factor(s.aspectRatio()) : 0.0;
|
||||
double baseStall = s.slab() ? SurfaceType.SLAB_STALL_ANGLE
|
||||
: s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle;
|
||||
double critical = Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
|
||||
double vortexFactor = vortexFactor(s, tuning);
|
||||
double critical = criticalAngle(s, tuning, vortexFactor);
|
||||
double absAlpha = Math.abs(alpha);
|
||||
if (alpha >= 0.0) {
|
||||
critical += FLAP_STALL_GAIN * flapShift;
|
||||
@@ -149,9 +146,8 @@ final class ImprovedAero {
|
||||
|
||||
// Finite wing theory: lift slope and induced drag follow from the wing's aspect ratio
|
||||
double aspectRatio = s.aspectRatio();
|
||||
double slope = aspectRatio > 0.0 ? 2.0 * Math.PI / (1.0 + 2.0 / aspectRatio) : LIFT_SLOPE;
|
||||
double inducedDrag = aspectRatio > 0.0 ? 1.0 / (Math.PI * EFFICIENCY * aspectRatio) : INDUCED_DRAG;
|
||||
double attachedLift = slope * alpha;
|
||||
double attachedLift = liftSlope(s) * alpha;
|
||||
if (s.type() == SurfaceType.WING) {
|
||||
attachedLift += VortexLift.liftCoefficient(alpha, vortexFactor, tuning.vortexLiftStrength());
|
||||
}
|
||||
@@ -175,6 +171,44 @@ final class ImprovedAero {
|
||||
return stalled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lift (along the surface's normal) the surface would make in the airflow it sees now at the highest angle of
|
||||
* attack it can fly without starting to stall, flaps included. Only meaningful for horizontal surfaces.
|
||||
*/
|
||||
static double maxLift(AeroSurface s, double[] v, double[] w, double[] r, double flap, double weight, Tuning tuning) {
|
||||
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 u = vx * s.facingX() + vz * s.facingZ();
|
||||
double planeSpeedSq = u * u + vy * vy;
|
||||
double vortexFactor = vortexFactor(s, tuning);
|
||||
double flapShift = FLAP_ALPHA_FACTOR * flap;
|
||||
double alpha = criticalAngle(s, tuning, vortexFactor) + FLAP_STALL_GAIN * flapShift - STALL_BLEND;
|
||||
double lift = liftSlope(s) * alpha;
|
||||
if (s.type() == SurfaceType.WING) {
|
||||
lift += VortexLift.liftCoefficient(alpha, vortexFactor, tuning.vortexLiftStrength());
|
||||
}
|
||||
return LIFT_CONSTANT * s.pressure() * weight * s.fluidFactor() * planeSpeedSq * lift * tuning.liftScale();
|
||||
}
|
||||
|
||||
private static double vortexFactor(AeroSurface s, Tuning tuning) {
|
||||
return tuning.vortex() && (s.type() == SurfaceType.WING || s.wingGroup() && s.type().controllable)
|
||||
? VortexLift.factor(s.aspectRatio()) : 0.0;
|
||||
}
|
||||
|
||||
/** Angle of attack (radians, camber and flaps included) at which the surface starts to stall, without flaps. */
|
||||
private static double criticalAngle(AeroSurface s, Tuning tuning, double vortexFactor) {
|
||||
double baseStall = s.slab() ? SurfaceType.SLAB_STALL_ANGLE
|
||||
: s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle;
|
||||
return Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
|
||||
}
|
||||
|
||||
/** Lift slope per radian from finite wing theory, from the wing's aspect ratio. */
|
||||
private static double liftSlope(AeroSurface s) {
|
||||
double aspectRatio = s.aspectRatio();
|
||||
return aspectRatio > 0.0 ? 2.0 * Math.PI / (1.0 + 2.0 / aspectRatio) : LIFT_SLOPE;
|
||||
}
|
||||
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,13 @@ public final class MixinPlugin implements IMixinConfigPlugin {
|
||||
|
||||
private boolean warium;
|
||||
private boolean futurism;
|
||||
private boolean geckolib;
|
||||
|
||||
@Override
|
||||
public void onLoad(String mixinPackage) {
|
||||
warium = supported("valkyrien_warium", WARIUM_VERSION, "Warium VS");
|
||||
futurism = supported("futurism", FUTURISM_VERSION, "Futurism");
|
||||
geckolib = LoadingModList.get().getModFileById("geckolib") != null;
|
||||
}
|
||||
|
||||
private static boolean supported(String modId, String version, String name) {
|
||||
@@ -49,6 +51,10 @@ public final class MixinPlugin implements IMixinConfigPlugin {
|
||||
if (mixinClassName.contains(".mixin.warium.")) {
|
||||
return warium;
|
||||
}
|
||||
if (mixinClassName.contains(".mixin.gecko.")) {
|
||||
// Animates Warium's control surfaces, which are GeckoLib models
|
||||
return warium && geckolib;
|
||||
}
|
||||
if (mixinClassName.contains(".mixin.futurism.")) {
|
||||
return futurism;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ public final class WariumCapture {
|
||||
world = level;
|
||||
pos = BlockPos.containing(x, y, z);
|
||||
state = blockState;
|
||||
if (surfaceType.controllable && level instanceof ServerLevel serverLevel && Config.PHYSICS_RATE_AERODYNAMICS.get()) {
|
||||
DeflectionSync.report(serverLevel, pos, surfaceType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package dev.lumareth.improvedAerodynamics.flight.mixin.gecko;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.client.animation.SurfaceAnimation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
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.callback.CallbackInfo;
|
||||
import software.bernie.geckolib.core.animatable.GeoAnimatable;
|
||||
import software.bernie.geckolib.core.animation.AnimationState;
|
||||
import software.bernie.geckolib.model.GeoModel;
|
||||
|
||||
/**
|
||||
* Runs after GeckoLib has played an animatable's animations, to put Warium's control surfaces at the deflection the
|
||||
* flight physics gives them, see {@link SurfaceAnimation}.
|
||||
*/
|
||||
@Mixin(value = GeoModel.class, remap = false)
|
||||
public abstract class GeoModelMixin {
|
||||
@Inject(method = "handleAnimations", at = @At("RETURN"))
|
||||
private void improvedAerodynamics$deflectSurface(GeoAnimatable animatable, long instanceId, AnimationState animationState, CallbackInfo ci) {
|
||||
if (animatable instanceof BlockEntity blockEntity) {
|
||||
SurfaceAnimation.apply(blockEntity, (GeoModel<?>) (Object) this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package dev.lumareth.improvedAerodynamics.network;
|
||||
|
||||
import dev.lumareth.improvedAerodynamics.client.animation.SurfaceAnimation;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** How far control surfaces are deflected, for the clients near a ship: block positions and tenths of a degree. */
|
||||
public final class DeflectionPacket {
|
||||
private final long[] positions;
|
||||
private final short[] tenths;
|
||||
|
||||
public DeflectionPacket(long[] positions, short[] tenths) {
|
||||
this.positions = positions;
|
||||
this.tenths = tenths;
|
||||
}
|
||||
|
||||
public void encode(FriendlyByteBuf buffer) {
|
||||
buffer.writeVarInt(positions.length);
|
||||
for (int i = 0; i < positions.length; i++) {
|
||||
buffer.writeLong(positions[i]);
|
||||
buffer.writeShort(tenths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static DeflectionPacket decode(FriendlyByteBuf buffer) {
|
||||
int count = buffer.readVarInt();
|
||||
long[] positions = new long[count];
|
||||
short[] tenths = new short[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
positions[i] = buffer.readLong();
|
||||
tenths[i] = buffer.readShort();
|
||||
}
|
||||
return new DeflectionPacket(positions, tenths);
|
||||
}
|
||||
|
||||
public void handle(Supplier<NetworkEvent.Context> context) {
|
||||
long[] pos = positions;
|
||||
short[] values = tenths;
|
||||
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
|
||||
for (int i = 0; i < pos.length; i++) {
|
||||
SurfaceAnimation.set(pos[i], values[i] / 10.0F);
|
||||
}
|
||||
});
|
||||
context.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,11 @@ public final class Network {
|
||||
.decoder(ControlModePacket::decode)
|
||||
.consumerMainThread(ControlModePacket::handle)
|
||||
.add();
|
||||
CHANNEL.messageBuilder(DeflectionPacket.class, 11, NetworkDirection.PLAY_TO_CLIENT)
|
||||
.encoder(DeflectionPacket::encode)
|
||||
.decoder(DeflectionPacket::decode)
|
||||
.consumerMainThread(DeflectionPacket::handle)
|
||||
.add();
|
||||
// Higher id than the neighbours so it does not collide with packets added on other branches
|
||||
CHANNEL.messageBuilder(FlapPacket.class, 10, NetworkDirection.PLAY_TO_SERVER)
|
||||
.encoder(FlapPacket::encode)
|
||||
|
||||
@@ -216,6 +216,18 @@
|
||||
{
|
||||
"id": "valkyrien_warium:green_control_surface_offset_top",
|
||||
"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,8 @@
|
||||
"compatibilityLevel": "JAVA_17",
|
||||
"mixins": [
|
||||
"futurism.ForceInducedShipsMixin",
|
||||
"futurism.VectoredAfterburnerSystemProcedureMixin",
|
||||
"gecko.GeoModelMixin",
|
||||
"warium.AirBrakeTickProcedureMixin",
|
||||
"warium.BypassedCompressorScriptProcedureMixin",
|
||||
"warium.ControlSurfaceTickProcedureMixin",
|
||||
|
||||
Reference in new issue
Block a user