Add Metalstorm-style cursor aim

Virtual stick cursor steered through the gun reticle, server-side virtual pilot
using measured control responses, client cursor/camera handling with look-ahead,
free look and scroll zoom.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
Claude committed 2026-11-11 13:38:11 +01:00
1 parent 6a8a7ead04
commit 44a4390f69
17 files changed
+1021 -22

No files matched your search

@@ -1,7 +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.flight.CursorAimConfig;
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
import dev.lumareth.improvedAerodynamics.network.Network;
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
@@ -23,6 +25,8 @@ public class ImprovedAerodynamics {
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC);
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");
Network.register();
}
}
@@ -0,0 +1,34 @@
package dev.lumareth.improvedAerodynamics.client.aim;
import com.mojang.blaze3d.platform.InputConstants;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import net.minecraft.client.KeyMapping;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraftforge.client.settings.KeyConflictContext;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.lwjgl.glfw.GLFW;
/** Keys of cursor aim. The free look key is not shift: in a seat, shift gets the player out of it. */
public final class AimKeys {
public static final KeyMapping TOGGLE = new KeyMapping("key.improved_aerodynamics.toggle_cursor_aim",
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_K, "key.categories.improved_aerodynamics");
public static final KeyMapping FREE_LOOK = new KeyMapping("key.improved_aerodynamics.cursor_aim_free_look",
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_LEFT_ALT, "key.categories.improved_aerodynamics");
private AimKeys() {
}
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
public static final class Registration {
private Registration() {
}
@SubscribeEvent
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
event.register(TOGGLE);
event.register(FREE_LOOK);
}
}
}
@@ -0,0 +1,15 @@
package dev.lumareth.improvedAerodynamics.client.aim;
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
import net.minecraft.world.entity.Entity;
/** Everything that touches Warium VS classes, so nothing else loads them when Warium is missing. */
final class AimSeat {
private AimSeat() {
}
static Entity seatOf(Entity rider) {
Entity root = rider.getRootVehicle();
return root instanceof SeatEntityEntity ? root : null;
}
}
@@ -0,0 +1,444 @@
package dev.lumareth.improvedAerodynamics.client.aim;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.BufferUploader;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.Tesselator;
import com.mojang.blaze3d.vertex.VertexFormat;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import dev.lumareth.improvedAerodynamics.client.pipper.GunPipperRenderer;
import dev.lumareth.improvedAerodynamics.flight.AimBasis;
import dev.lumareth.improvedAerodynamics.network.AimPacket;
import dev.lumareth.improvedAerodynamics.network.Network;
import net.minecraft.client.Camera;
import net.minecraft.client.CameraType;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import net.minecraftforge.client.event.ViewportEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import org.joml.Matrix4f;
import org.joml.Quaterniondc;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.valkyrienskies.core.api.ships.ClientShip;
import org.valkyrienskies.core.api.ships.Ship;
import org.valkyrienskies.mod.common.VSGameUtilsKt;
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 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>
* </ul>
* The camera is driven through the rotation of the player, which for a rider of a Valkyrien Skies ship is relative to
* the ship, so the camera follows the ship's attitude like Valkyrien Skies does for any mounted player. Mouse
* movement shows up as a change of that rotation, which is taken as the movement of the cursor and put back.
*/
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
public final class CursorAimClient {
private static final int CURSOR_COLOR = 0xFFFFE040;
private static final int RETICLE_COLOR = 0xFFFFFFFF;
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;
private static boolean active;
// Cursor as components along the craft's forward (gun reticle), right and up axes
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;
private static double offsetYaw;
private static double offsetPitch;
// Rotation the player was given last frame, to see what the mouse did since
private static boolean haveLast;
private static float lastYaw;
private static float lastPitch;
private static long lastNanos;
private static double zoom;
private static CameraType previousCamera;
private static Method cameraMove;
private CursorAimClient() {
}
// ---- Toggling and the packet to the server ----
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase != TickEvent.Phase.END) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
LocalPlayer player = minecraft.player;
while (AimKeys.TOGGLE.consumeClick()) {
if (active) {
deactivate(minecraft);
} else if (player != null) {
activate(minecraft, player);
}
}
if (!active) {
return;
}
if (player == null || !seated(player)) {
deactivate(minecraft);
return;
}
send(minecraft, true);
}
private static boolean seated(LocalPlayer player) {
return ModList.get().isLoaded("valkyrien_warium") && AimSeat.seatOf(player) != null;
}
private static boolean serverHasMod(Minecraft minecraft) {
return minecraft.getConnection() != null && Network.CHANNEL.isRemotePresent(minecraft.getConnection().getConnection());
}
private static void activate(Minecraft minecraft, LocalPlayer player) {
if (!seated(player)) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.no_seat"), true);
return;
}
if (!serverHasMod(minecraft)) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.no_server"), true);
return;
}
active = true;
cf = 1.0;
cr = 0.0;
cu = 0.0;
cameraDirection = null;
offsetYaw = 0.0;
offsetPitch = 0.0;
haveLast = false;
lastNanos = System.nanoTime();
previousCamera = minecraft.options.getCameraType();
minecraft.options.setCameraType(CameraType.THIRD_PERSON_BACK);
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.on"), true);
}
private static void deactivate(Minecraft minecraft) {
if (!active) {
return;
}
active = false;
send(minecraft, false);
if (previousCamera != null) {
minecraft.options.setCameraType(previousCamera);
previousCamera = null;
}
if (minecraft.player != null) {
minecraft.player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.off"), true);
}
}
private static void send(Minecraft minecraft, boolean on) {
if (serverHasMod(minecraft)) {
Network.CHANNEL.sendToServer(new AimPacket(on, (float) cf, (float) cr, (float) cu));
}
}
// ---- Mouse and camera, once per frame before the world is rendered ----
@SubscribeEvent
public static void onRenderTick(TickEvent.RenderTickEvent event) {
if (!active || event.phase != TickEvent.Phase.START) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
LocalPlayer player = minecraft.player;
ClientLevel level = minecraft.level;
if (player == null || level == null || !seated(player)) {
return;
}
Entity seat = AimSeat.seatOf(player);
AimBasis basis = AimBasis.of(level, seat.blockPosition());
if (basis == null) {
return;
}
if (minecraft.options.getCameraType() != CameraType.THIRD_PERSON_BACK) {
minecraft.options.setCameraType(CameraType.THIRD_PERSON_BACK);
}
long now = System.nanoTime();
double dt = Mth.clamp((now - lastNanos) / 1.0E9, 0.0, 0.1);
lastNanos = now;
// What the mouse did: the player's rotation moved away from what it was set to
double deltaYaw = 0.0;
double deltaPitch = 0.0;
if (haveLast && minecraft.screen == null) {
deltaYaw = Mth.wrapDegrees(player.getYRot() - lastYaw);
deltaPitch = player.getXRot() - lastPitch;
if (Math.abs(deltaYaw) > 90.0 || Math.abs(deltaPitch) > 90.0) {
// Not the mouse, something reset the rotation
deltaYaw = 0.0;
deltaPitch = 0.0;
}
}
double sensitivity = CursorAimClientConfig.MOUSE_SENSITIVITY.get();
boolean freeLook = minecraft.screen == null && AimKeys.FREE_LOOK.isDown();
double spring = CursorAimClientConfig.SPRING_BACK_SECONDS.get();
if (spring > 0.0) {
shrinkCursor(Math.exp(-dt / spring));
}
if (freeLook) {
offsetYaw += deltaYaw * sensitivity;
offsetPitch = Mth.clamp(offsetPitch - deltaPitch * sensitivity, -89.0, 89.0);
} else {
moveCursor(Math.toRadians(deltaYaw * sensitivity), Math.toRadians(-deltaPitch * sensitivity));
double decay = Math.exp(-dt * FREE_LOOK_RETURN_RATE);
offsetYaw *= decay;
offsetPitch *= decay;
}
// Camera: along the reticle, or turned towards the cursor by the look ahead setting
double lookAhead = CursorAimClientConfig.LOOK_AHEAD.get();
double[] forward = basis.forward();
if (cameraDirection == null || lookAhead <= 0.0) {
cameraDirection = forward.clone();
} else {
double[] target = basis.toShip(cf, cr, cu);
cameraDirection = turnTowards(cameraDirection, target, 1.0 - Math.exp(-lookAhead * LOOK_AHEAD_RATE * dt));
}
double[] direction = rotate(cameraDirection, basis.up(), -Math.toRadians(offsetYaw));
direction = rotate(direction, rotate(basis.right(), basis.up(), -Math.toRadians(offsetYaw)), Math.toRadians(offsetPitch));
// Valkyrien Skies reads the view of a mounted player in ship space: yaw 0 looks along +Z
float yaw = (float) Math.toDegrees(Math.atan2(-direction[0], direction[2]));
float pitch = (float) Math.toDegrees(Math.atan2(-direction[1], Math.hypot(direction[0], direction[2])));
player.setYRot(yaw);
player.setXRot(pitch);
player.yRotO = yaw;
player.xRotO = pitch;
player.yHeadRot = yaw;
player.yHeadRotO = yaw;
lastYaw = yaw;
lastPitch = pitch;
haveLast = true;
}
/** Moves the cursor like a look direction: yaw towards the right, pitch up, radians. */
private static void moveCursor(double yaw, double pitch) {
double nf = cf * Math.cos(yaw) - cr * Math.sin(yaw);
double nr = cf * Math.sin(yaw) + cr * Math.cos(yaw);
cf = nf;
cr = nr;
nf = cf * Math.cos(pitch) - cu * Math.sin(pitch);
double nu = cf * Math.sin(pitch) + cu * Math.cos(pitch);
cf = nf;
cu = nu;
normalizeCursor();
limitCursor();
}
/** Brings the cursor back towards the reticle along the shortest way, keeping only this fraction of its angle. */
private static void shrinkCursor(double keep) {
double angle = Math.acos(Mth.clamp(cf, -1.0, 1.0));
double lateral = Math.sqrt(cr * cr + cu * cu);
if (angle < 1.0E-6 || lateral < 1.0E-9) {
cf = 1.0;
cr = 0.0;
cu = 0.0;
return;
}
double newAngle = angle * keep;
double scale = Math.sin(newAngle) / lateral;
cf = Math.cos(newAngle);
cr *= scale;
cu *= scale;
}
private static void normalizeCursor() {
double length = Math.sqrt(cf * cf + cr * cr + cu * cu);
if (!(length > 1.0E-9)) {
cf = 1.0;
cr = 0.0;
cu = 0.0;
return;
}
cf /= length;
cr /= length;
cu /= length;
}
private static void limitCursor() {
double limit = Math.toRadians(CursorAimClientConfig.MAX_CURSOR_ANGLE_DEGREES.get());
double angle = Math.acos(Mth.clamp(cf, -1.0, 1.0));
if (angle <= limit) {
return;
}
double lateral = Math.sqrt(cr * cr + cu * cu);
if (lateral < 1.0E-9) {
return;
}
double scale = Math.sin(limit) / lateral;
cf = Math.cos(limit);
cr *= scale;
cu *= scale;
}
// ---- Mouse wheel and camera distance ----
@SubscribeEvent
public static void onScroll(InputEvent.MouseScrollingEvent event) {
Minecraft minecraft = Minecraft.getInstance();
if (!active || minecraft.screen != null) {
return;
}
zoom = Mth.clamp(zoom - event.getScrollDelta() * CursorAimClientConfig.ZOOM_STEP.get(), -3.0, CursorAimClientConfig.ZOOM_MAX.get());
event.setCanceled(true);
}
@SubscribeEvent
public static void onCameraAngles(ViewportEvent.ComputeCameraAngles event) {
if (!active || Math.abs(zoom) < 1.0E-3) {
return;
}
try {
if (cameraMove == null) {
// Camera.move(double, double, double), moves along the camera's own axes
cameraMove = ObfuscationReflectionHelper.findMethod(Camera.class, "m_90568_", double.class, double.class, double.class);
}
cameraMove.invoke(event.getCamera(), -zoom, 0.0, 0.0);
} catch (ReflectiveOperationException | RuntimeException exception) {
ImprovedAerodynamics.LOGGER.warn("Cursor aim camera zoom is not available", exception);
zoom = 0.0;
}
}
// ---- Drawing the cursor ----
@SubscribeEvent
public static void onRenderLevel(RenderLevelStageEvent event) {
if (!active || event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
ClientLevel level = minecraft.level;
LocalPlayer player = minecraft.player;
if (level == null || player == null || !seated(player)) {
return;
}
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) {
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]));
Camera camera = event.getCamera();
Matrix4f matrix = event.getPoseStack().last().pose();
BufferBuilder builder = Tesselator.getInstance().getBuilder();
RenderSystem.disableDepthTest();
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
RenderSystem.disableCull();
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()) {
plus(builder, matrix, camera, reticleWorld, size * 0.35F, RETICLE_COLOR);
}
ring(builder, matrix, camera, cursorWorld, size, CURSOR_COLOR);
BufferUploader.drawWithShader(builder.end());
RenderSystem.enableCull();
RenderSystem.enableDepthTest();
RenderSystem.disableBlend();
}
private static Vector3f screenPoint(Camera camera, Vector3d direction) {
Vector3f center = new Vector3f((float) direction.x, (float) direction.y, (float) direction.z);
if (center.lengthSquared() < 1.0E-9F) {
return null;
}
return center.normalize().mul(MARKER_DISTANCE);
}
private static void ring(BufferBuilder builder, Matrix4f matrix, Camera camera, Vector3d direction, float radius, int color) {
Vector3f center = screenPoint(camera, direction);
if (center == null) {
return;
}
Vector3f left = new Vector3f(camera.getLeftVector());
Vector3f up = new Vector3f(camera.getUpVector());
float thickness = radius * 0.12F;
GunPipperRenderer.ring(builder, matrix, center, left, up, radius, thickness, color);
GunPipperRenderer.quad(builder, matrix, center, left, up, thickness * 1.4F, thickness * 1.4F, color);
}
private static void plus(BufferBuilder builder, Matrix4f matrix, Camera camera, Vector3d direction, float arm, int color) {
Vector3f center = screenPoint(camera, direction);
if (center == null) {
return;
}
Vector3f left = new Vector3f(camera.getLeftVector());
Vector3f up = new Vector3f(camera.getUpVector());
float thickness = arm * 0.14F;
GunPipperRenderer.quad(builder, matrix, center, left, up, arm, thickness, color);
GunPipperRenderer.quad(builder, matrix, center, left, up, thickness, arm, color);
}
// ---- Vector helpers on arrays of three ----
/** Rotates v about the unit axis by an angle in radians (Rodrigues). */
private static double[] rotate(double[] v, double[] axis, double angle) {
double c = Math.cos(angle);
double s = Math.sin(angle);
double dot = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
double cx = axis[1] * v[2] - axis[2] * v[1];
double cy = axis[2] * v[0] - axis[0] * v[2];
double cz = axis[0] * v[1] - axis[1] * v[0];
return new double[]{
v[0] * c + cx * s + axis[0] * dot * (1.0 - c),
v[1] * c + cy * s + axis[1] * dot * (1.0 - c),
v[2] * c + cz * s + axis[2] * dot * (1.0 - c)};
}
/** Turns unit vector a towards unit vector b by a fraction of the angle between them. */
private static double[] turnTowards(double[] a, double[] b, double fraction) {
double dot = Mth.clamp(a[0] * b[0] + a[1] * b[1] + a[2] * b[2], -1.0, 1.0);
double cx = a[1] * b[2] - a[2] * b[1];
double cy = a[2] * b[0] - a[0] * b[2];
double cz = a[0] * b[1] - a[1] * b[0];
double sin = Math.sqrt(cx * cx + cy * cy + cz * cz);
if (sin < 1.0E-9) {
return dot > 0.0 ? b.clone() : a.clone();
}
double[] axis = {cx / sin, cy / sin, cz / sin};
return rotate(a, axis, Math.acos(dot) * fraction);
}
}
@@ -0,0 +1,45 @@
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() {
}
}
@@ -218,7 +218,7 @@ public final class GunPipperRenderer {
return true;
}
private static void ring(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float radius, float thickness, int color) {
public static void ring(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float radius, float thickness, int color) {
final int segments = 32;
float inner = radius - thickness;
float outer = radius + thickness;
@@ -232,7 +232,7 @@ public final class GunPipperRenderer {
}
}
private static void quad(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float halfX, float halfY, int color) {
public static void quad(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float halfX, float halfY, int color) {
vertex(builder, matrix, center, left, up, -halfX, -halfY, color);
vertex(builder, matrix, center, left, up, halfX, -halfY, color);
vertex(builder, matrix, center, left, up, halfX, halfY, color);
@@ -27,6 +27,7 @@ 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;
private static final ControlAxis[] MEASURED_AXES = {ControlAxis.PITCH, ControlAxis.YAW, ControlAxis.ROLL};
private final SubstepClock clock;
@@ -57,9 +58,22 @@ public final class AeroState implements PhysicsBridge.Publishable {
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
final Vector3d pitchResponse = new Vector3d();
final Vector3d pitchResponseBuilding = new Vector3d();
// The same for yaw and roll, measured while cursor aim flies the node
final Vector3d yawResponse = new Vector3d();
final Vector3d yawResponseBuilding = new Vector3d();
final Vector3d rollResponse = new Vector3d();
final Vector3d rollResponseBuilding = new Vector3d();
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
volatile double savedTrim;
Vector3d responseBuilding(ControlAxis axis) {
return switch (axis) {
case YAW -> yawResponseBuilding;
case ROLL -> rollResponseBuilding;
default -> pitchResponseBuilding;
};
}
NodeState(NodeData data) {
pitch = data.pitch();
yaw = data.yaw();
@@ -211,6 +225,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
wingStalled = false;
for (NodeState state : nodeStates.values()) {
state.pitchResponseBuilding.zero();
state.yawResponseBuilding.zero();
state.rollResponseBuilding.zero();
}
}
@@ -221,7 +237,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
r[2] = s.z() - centerOfMass.z();
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null;
NodeState state = node != null ? nodeStates.get(node.key()) : null;
double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0;
double input = s.type().controllable ? input(s, node, state, ControlAxis.NONE, 0.0) : 0.0;
if (improved) {
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
@@ -233,11 +249,16 @@ public final class AeroState implements PhysicsBridge.Publishable {
WariumAero.evaluate(s, v, w, r, input, weight, out);
}
// How much a little more pitch input on this node would change the torque, for auto trim
if (commit && node != null && node.autoTrim() && s.type().controllable && s.listensToPitch()) {
// How much a little more input on an axis would change the torque: pitch for auto trim, all three for
// cursor aim
if (commit && node != null && s.type().controllable) {
boolean aimed = CursorAimRegistry.isActive(node.key());
for (ControlAxis axis : MEASURED_AXES) {
if (!s.listensTo(axis) || !(aimed || (axis == ControlAxis.PITCH && node.autoTrim()))) {
continue;
}
double delta = 0.05;
probe.clear();
double base = 0.0;
if (improved) {
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
} else {
@@ -245,41 +266,45 @@ public final class AeroState implements PhysicsBridge.Publishable {
}
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
probe.clear();
double nudged = input(s, node, state, delta);
double nudged = input(s, node, state, axis, delta);
if (improved) {
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
}
state.pitchResponseBuilding.add(
state.responseBuilding(axis).add(
(probe.totalTorque(0) - before[0]) / delta,
(probe.totalTorque(1) - before[1]) / delta,
(probe.totalTorque(2) - before[2]) / delta);
}
}
}
if (commit) {
for (NodeState state : nodeStates.values()) {
state.pitchResponse.set(state.pitchResponseBuilding);
state.yawResponse.set(state.yawResponseBuilding);
state.rollResponse.set(state.rollResponseBuilding);
}
}
}
/** A control surface's input, combined from its two keys and clamped like Warium does. */
private static double input(AeroSurface s, NodeData node, NodeState state, double extraPitch) {
private static double input(AeroSurface s, NodeData node, NodeState state, ControlAxis nudged, double extra) {
if (node == null || state == null) {
return 0.0;
}
double value = axisValue(s.axis1(), node, state, extraPitch) * s.multiplier1()
+ axisValue(s.axis2(), node, state, extraPitch) * s.multiplier2();
double value = axisValue(s.axis1(), node, state, nudged, extra) * s.multiplier1()
+ axisValue(s.axis2(), node, state, nudged, extra) * s.multiplier2();
return Math.max(-1.0, Math.min(1.0, value));
}
private static double axisValue(ControlAxis axis, NodeData node, NodeState state, double extraPitch) {
private static double axisValue(ControlAxis axis, NodeData node, NodeState state, ControlAxis nudged, double extra) {
double nudge = axis == nudged ? extra : 0.0;
return switch (axis) {
case PITCH -> state.pitch + state.trimOutput + extraPitch;
case YAW -> state.yaw;
case ROLL -> state.roll;
case PITCH -> state.pitch + state.trimOutput + nudge;
case YAW -> state.yaw + nudge;
case ROLL -> state.roll + nudge;
case THROTTLE -> node.throttle();
case NONE -> 0.0;
};
@@ -293,10 +318,12 @@ public final class AeroState implements PhysicsBridge.Publishable {
if (state == null) {
continue;
}
state.pitch = moveTowards(state.pitch, node.pitch(), step);
state.yaw = moveTowards(state.yaw, node.yaw(), step);
state.roll = moveTowards(state.roll, node.roll(), step);
AutoTrim.update(state, node, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
// 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);
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
state.roll = moveTowards(state.roll, effective.roll(), step);
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
}
}
@@ -36,6 +36,10 @@ public record AeroSurface(long key, SurfaceType type, boolean slab, double x, do
}
boolean listensToPitch() {
return node != NO_NODE && (axis1 == ControlAxis.PITCH || axis2 == ControlAxis.PITCH);
return listensTo(ControlAxis.PITCH);
}
boolean listensTo(ControlAxis axis) {
return node != NO_NODE && (axis1 == axis || axis2 == axis);
}
}
@@ -0,0 +1,101 @@
package dev.lumareth.improvedAerodynamics.flight;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.block.state.properties.Property;
/**
* The frame cursor aim steers in, in ship coordinates: forward is the boresight (what Warium's gun reticle points
* along, from the HUD block in front of the seat, else the way the seat faces), up is the ship's up axis and right
* completes it. Server and client both build it from the same block data, so a cursor sent as components in this
* frame means the same on both.
*
* @param nodePos the Vehicle Control Node the seat is linked to
*/
public record AimBasis(BlockPos seatPos, BlockPos nodePos, double[] forward, double[] right, double[] up) {
/**
* @param entityPos block position of the seat entity. Warium's control seat block is at that position or just
* below it.
* @return null if there is no seat block linked to a control node there
*/
public static AimBasis of(Level level, BlockPos entityPos) {
for (BlockPos pos : new BlockPos[]{entityPos, entityPos.below()}) {
BlockEntity seat = level.getBlockEntity(pos);
if (seat == null || !seat.getPersistentData().contains("ControlX")) {
continue;
}
CompoundTag tag = seat.getPersistentData();
BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ"));
return new AimBasis(pos, nodePos, null, null, null).withAxes(level, pos, nodePos);
}
return null;
}
private AimBasis withAxes(Level level, BlockPos seatPos, BlockPos nodePos) {
BlockState state = level.getBlockState(seatPos);
Direction facing = Direction.NORTH;
Property<?> property = state.getBlock().getStateDefinition().getProperty("facing");
if (property instanceof DirectionProperty directionProperty) {
facing = state.getValue(directionProperty);
}
double fx = facing.getStepX();
double fy = facing.getStepY();
double fz = facing.getStepZ();
// Warium's gun reticle: the HUD block in front of the seat holds the aim vector of its guns
BlockEntity hud = level.getBlockEntity(seatPos.relative(facing));
if (hud != null && hud.getPersistentData().contains("Pitch")) {
CompoundTag hudTag = hud.getPersistentData();
double bx = hudTag.getDouble("X") + facing.getStepX();
double by = hudTag.getDouble("Pitch");
double bz = hudTag.getDouble("Z") + facing.getStepZ();
if (bx * bx + by * by + bz * bz > 1.0E-6) {
fx = bx;
fy = by;
fz = bz;
}
}
double length = Math.sqrt(fx * fx + fy * fy + fz * fz);
if (length < 1.0E-6) {
fx = 0.0;
fy = 0.0;
fz = -1.0;
length = 1.0;
}
fx /= length;
fy /= length;
fz /= length;
// right = forward x up(0,1,0)
double rx = -fz;
double ry = 0.0;
double rz = fx;
double rl = Math.sqrt(rx * rx + rz * rz);
if (rl < 1.0E-6) {
rx = 1.0;
rz = 0.0;
rl = 1.0;
}
rx /= rl;
rz /= rl;
// up = right x forward
double ux = ry * fz - rz * fy;
double uy = rz * fx - rx * fz;
double uz = rx * fy - ry * fx;
return new AimBasis(seatPos, nodePos, new double[]{fx, fy, fz}, new double[]{rx, ry, rz}, new double[]{ux, uy, uz});
}
/** Ship-space direction of a cursor given by its components along forward, right and up. */
public double[] toShip(double cf, double cr, double cu) {
return new double[]{
forward[0] * cf + right[0] * cr + up[0] * cu,
forward[1] * cf + right[1] * cr + up[1] * cu,
forward[2] * cf + right[2] * cr + up[2] * cu};
}
}
@@ -0,0 +1,52 @@
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 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);
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() {
}
}
@@ -0,0 +1,44 @@
package dev.lumareth.improvedAerodynamics.flight;
import java.util.concurrent.ConcurrentHashMap;
/**
* Where each control node's pilot currently holds the cursor of cursor aim. Written by the server thread when the
* client's packet arrives, read by the physics thread. An entry that is not refreshed for a moment (the player left
* the seat, lost the connection) counts as gone.
*/
public final class CursorAimRegistry {
private static final long EXPIRE_NANOS = 500_000_000L;
private static final ConcurrentHashMap<Long, Aim> AIMS = new ConcurrentHashMap<>();
/** Cursor as components along the basis axes (unit length), and the basis itself, in ship space. */
public record Aim(double cf, double cr, double cu, double[] forward, double[] right, double[] up, long nanos) {
}
private CursorAimRegistry() {
}
public static void set(long nodeKey, Aim aim) {
AIMS.put(nodeKey, aim);
}
public static void clear(long nodeKey) {
AIMS.remove(nodeKey);
}
public static Aim get(long nodeKey) {
Aim aim = AIMS.get(nodeKey);
if (aim == null) {
return null;
}
if (System.nanoTime() - aim.nanos() > EXPIRE_NANOS) {
AIMS.remove(nodeKey, aim);
return null;
}
return aim;
}
public static boolean isActive(long nodeKey) {
return get(nodeKey) != null;
}
}
@@ -0,0 +1,30 @@
package dev.lumareth.improvedAerodynamics.flight;
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
/** Server side of cursor aim: takes the cursor a pilot sends and hands it to the control node's virtual pilot. */
public final class CursorAimServer {
private CursorAimServer() {
}
public static void accept(ServerPlayer player, boolean active, double cf, double cr, double cu) {
Entity vehicle = player.getVehicle();
if (!(vehicle instanceof SeatEntityEntity)) {
return;
}
AimBasis basis = AimBasis.of(vehicle.level(), vehicle.blockPosition());
if (basis == null) {
return;
}
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)) {
CursorAimRegistry.clear(nodeKey);
return;
}
CursorAimRegistry.set(nodeKey, new CursorAimRegistry.Aim(cf / length, cr / length, cu / length,
basis.forward(), basis.right(), basis.up(), System.nanoTime()));
}
}
@@ -0,0 +1,136 @@
package dev.lumareth.improvedAerodynamics.flight;
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:
* <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>
* </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.
*/
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);
private 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()) {
return node;
}
CursorAimRegistry.Aim aim = CursorAimRegistry.get(node.key());
if (aim == null || speed < CursorAimConfig.MIN_SPEED.get()) {
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[] 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()));
double lateral = saturate(sideways / fullAngle);
double blend = smoothstep(Math.abs(sideways), FINE_START, FINE_END);
// 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));
}
if (wingStalled && pitchRate > 0.0) {
pitchRate *= CursorAimConfig.STALLED_PITCH_FACTOR.get();
}
// Close to the reticle the nose follows the cursor with the rudder
double yawRate = maxTurn * lateral * (1.0 - blend);
// Wanted angular velocity in ship axes: roll about forward, pitch about right, yaw (nose right) about -up
Vector3d wanted = new Vector3d(
rollRate * f[0] + pitchRate * r[0] - yawRate * u[0],
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 torque = inertia.transform(acceleration, new Vector3d());
double[] inputs = solve(state.pitchResponse, state.yawResponse, state.rollResponse, torque);
if (inputs == null) {
return node;
}
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 new NodeData(node.key(),
Math.abs(node.pitch()) > MANUAL_OVERRIDE ? node.pitch() : pitch,
Math.abs(node.yaw()) > MANUAL_OVERRIDE ? node.yaw() : yaw,
Math.abs(node.roll()) > MANUAL_OVERRIDE ? node.roll() : roll,
node.throttle(), node.autoTrim(), node.storedTrim());
}
/** 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);
Matrix3d transposed = responses.transpose(new Matrix3d());
Matrix3d normal = transposed.mul(responses, new Matrix3d());
double trace = normal.m00() + normal.m11() + normal.m22();
if (!(trace > 1.0E-9) || !Double.isFinite(trace)) {
return null;
}
double ridge = trace / 3.0 * 1.0E-3;
normal.m00(normal.m00() + ridge);
normal.m11(normal.m11() + ridge);
normal.m22(normal.m22() + ridge);
Vector3d right = transposed.transform(torque, new Vector3d());
Vector3d x = normal.invert().transform(right);
if (!x.isFinite()) {
return null;
}
return new double[]{x.x, x.y, x.z};
}
private static double saturate(double value) {
return clamp(value, -1.0, 1.0);
}
private static double smoothstep(double value, double from, double to) {
double t = clamp((value - from) / (to - from), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
private static double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
}
@@ -0,0 +1,45 @@
package dev.lumareth.improvedAerodynamics.network;
import dev.lumareth.improvedAerodynamics.flight.CursorAimServer;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
/**
* Sent every tick while a pilot has cursor aim on, and once when it goes off. The cursor is given as components along
* the craft's boresight, right and up axes, so it does not depend on the camera.
*/
public final class AimPacket {
private final boolean active;
private final float forward;
private final float right;
private final float up;
public AimPacket(boolean active, float forward, float right, float up) {
this.active = active;
this.forward = forward;
this.right = right;
this.up = up;
}
public void encode(FriendlyByteBuf buffer) {
buffer.writeBoolean(active);
buffer.writeFloat(forward);
buffer.writeFloat(right);
buffer.writeFloat(up);
}
public static AimPacket decode(FriendlyByteBuf buffer) {
return new AimPacket(buffer.readBoolean(), buffer.readFloat(), buffer.readFloat(), buffer.readFloat());
}
public void handle(Supplier<NetworkEvent.Context> context) {
ServerPlayer player = context.get().getSender();
if (player != null && ModList.get().isLoaded("valkyrien_warium")) {
CursorAimServer.accept(player, active, forward, right, up);
}
}
}
@@ -23,5 +23,11 @@ public final class Network {
.decoder(ToggleAutoTrimPacket::decode)
.consumerMainThread(ToggleAutoTrimPacket::handle)
.add();
// High id so it cannot collide with packets added on other branches
CHANNEL.messageBuilder(AimPacket.class, 50, NetworkDirection.PLAY_TO_SERVER)
.encoder(AimPacket::encode)
.decoder(AimPacket::decode)
.consumerMainThread(AimPacket::handle)
.add();
}
}
@@ -12,5 +12,11 @@
"config.improved_aerodynamics.supersonicDrag": "Multiplikator für den Nullauftriebswiderstand bei vollem Überschall, ab Mach 1,3",
"config.improved_aerodynamics.vortexLift": "Wirbelauftrieb: Flügel mit kleiner Streckung (Deltaflügel) überziehen später und liefern bei großen Anstellwinkeln mehr Auftrieb (verbessertes Flugmodell)",
"config.improved_aerodynamics.vortexStallBonus": "Zusätzlicher Überziehwinkel der Flügel mit der kleinsten Streckung, als Anteil des normalen Überziehwinkels",
"config.improved_aerodynamics.vortexLiftStrength": "Multiplikator für den zusätzlichen Auftrieb der Vorderkantenwirbel"
"config.improved_aerodynamics.vortexLiftStrength": "Multiplikator für den zusätzlichen Auftrieb der Vorderkantenwirbel",
"key.improved_aerodynamics.toggle_cursor_aim": "Cursor-Steuerung umschalten",
"key.improved_aerodynamics.cursor_aim_free_look": "Cursor-Steuerung: Freie Sicht (halten)",
"message.improved_aerodynamics.cursor_aim.on": "Cursor-Steuerung an",
"message.improved_aerodynamics.cursor_aim.off": "Cursor-Steuerung aus",
"message.improved_aerodynamics.cursor_aim.no_seat": "Setz dich in einen Warium-Steuersitz, um die Cursor-Steuerung zu nutzen",
"message.improved_aerodynamics.cursor_aim.no_server": "Dieser Server hat Improved Aerodynamics nicht, die Cursor-Steuerung braucht es"
}
@@ -12,5 +12,11 @@
"config.improved_aerodynamics.supersonicDrag": "Zero-lift drag multiplier once fully supersonic, from Mach 1.3",
"config.improved_aerodynamics.vortexLift": "Vortex lift: low aspect ratio (delta) wings stall later and lift more at high angles of attack (improved flight model)",
"config.improved_aerodynamics.vortexStallBonus": "Extra stall angle of the lowest aspect ratio wings, as a fraction of the normal stall angle",
"config.improved_aerodynamics.vortexLiftStrength": "Multiplier for the extra lift the leading-edge vortices make"
"config.improved_aerodynamics.vortexLiftStrength": "Multiplier for the extra lift the leading-edge vortices make",
"key.improved_aerodynamics.toggle_cursor_aim": "Toggle cursor aim",
"key.improved_aerodynamics.cursor_aim_free_look": "Cursor aim: free look (hold)",
"message.improved_aerodynamics.cursor_aim.on": "Cursor aim on",
"message.improved_aerodynamics.cursor_aim.off": "Cursor aim off",
"message.improved_aerodynamics.cursor_aim.no_seat": "Sit in a Warium control seat to use cursor aim",
"message.improved_aerodynamics.cursor_aim.no_server": "This server does not have Improved Aerodynamics, cursor aim needs it"
}