Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
433311a1c9 | ||
|
|
0c8af898ae | ||
|
|
3c7889796b | ||
|
|
44a4390f69 |
No files matched your search
@@ -5,6 +5,7 @@ import com.mojang.brigadier.arguments.BoolArgumentType;
|
|||||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||||
|
import dev.lumareth.improvedAerodynamics.flight.ControlMode;
|
||||||
import net.minecraft.commands.CommandSourceStack;
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
import net.minecraft.commands.Commands;
|
import net.minecraft.commands.Commands;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@@ -18,7 +19,8 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* /improvedaerodynamics (or /aero) to read and change the most important server config values in game.
|
* /improvedaerodynamics (or /aero) to read and change the most important server config values in game.
|
||||||
* {@code /aero} lists them, {@code /aero <option>} shows one, {@code /aero <option> <value>} sets and saves it.
|
* {@code /aero} lists them, {@code /aero <option>} shows one, {@code /aero <option> <value>} sets and saves it.
|
||||||
* Needs operator rights, except for the owner of a singleplayer world, who may use them without cheats.
|
* The config options need operator rights, except for the owner of a singleplayer world, who may use them without cheats.
|
||||||
|
* {@code /aero controls keyboard|cursor_aim} is open to every player and picks how they fly.
|
||||||
* All of them are read live, so changes apply immediately.
|
* All of them are read live, so changes apply immediately.
|
||||||
*/
|
*/
|
||||||
public final class ConfigCommands {
|
public final class ConfigCommands {
|
||||||
@@ -60,19 +62,45 @@ public final class ConfigCommands {
|
|||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void onRegisterCommands(RegisterCommandsEvent event) {
|
public static void onRegisterCommands(RegisterCommandsEvent event) {
|
||||||
|
// The root is open to everybody for /aero controls, the config options check for rights themselves
|
||||||
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics")
|
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics")
|
||||||
.requires(ConfigCommands::mayUse)
|
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource()));
|
||||||
.executes(context -> list(context.getSource()));
|
|
||||||
for (Option<?> option : OPTIONS) {
|
for (Option<?> option : OPTIONS) {
|
||||||
root.then(optionNode(option));
|
root.then(optionNode(option).requires(ConfigCommands::mayUse));
|
||||||
}
|
}
|
||||||
|
root.then(controlsNode());
|
||||||
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
|
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
|
||||||
event.getDispatcher().register(Commands.literal("aero")
|
event.getDispatcher().register(Commands.literal("aero")
|
||||||
.requires(ConfigCommands::mayUse)
|
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource()))
|
||||||
.executes(context -> list(context.getSource()))
|
|
||||||
.redirect(node));
|
.redirect(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int denied(CommandSourceStack source) {
|
||||||
|
source.sendFailure(Component.literal("Use /aero controls keyboard or /aero controls cursor_aim"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** /aero controls [keyboard|cursor_aim], for every player (permission level 0). */
|
||||||
|
private static LiteralArgumentBuilder<CommandSourceStack> controlsNode() {
|
||||||
|
return Commands.literal("controls")
|
||||||
|
.requires(source -> source.getEntity() instanceof ServerPlayer)
|
||||||
|
.executes(context -> {
|
||||||
|
ServerPlayer player = context.getSource().getPlayerOrException();
|
||||||
|
boolean cursorAim = ControlMode.isCursorAim(player);
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal("Controls: " + (cursorAim ? "cursor_aim" : "keyboard")), false);
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.then(Commands.literal("keyboard").executes(context -> setControls(context.getSource(), false)))
|
||||||
|
.then(Commands.literal("cursor_aim").executes(context -> setControls(context.getSource(), true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int setControls(CommandSourceStack source, boolean cursorAim) throws com.mojang.brigadier.exceptions.CommandSyntaxException {
|
||||||
|
ServerPlayer player = source.getPlayerOrException();
|
||||||
|
ControlMode.set(player, cursorAim);
|
||||||
|
source.sendSuccess(() -> Component.literal("Controls set to " + (cursorAim ? "cursor_aim" : "keyboard")), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
private static <T> LiteralArgumentBuilder<CommandSourceStack> optionNode(Option<T> option) {
|
private static <T> LiteralArgumentBuilder<CommandSourceStack> optionNode(Option<T> option) {
|
||||||
return Commands.literal(option.name())
|
return Commands.literal(option.name())
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package dev.lumareth.improvedAerodynamics;
|
package dev.lumareth.improvedAerodynamics;
|
||||||
|
|
||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
|
import dev.lumareth.improvedAerodynamics.client.aim.CursorAimClientConfig;
|
||||||
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
|
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
|
||||||
|
import dev.lumareth.improvedAerodynamics.flight.CursorAimConfig;
|
||||||
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
||||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||||
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
||||||
@@ -21,8 +23,11 @@ public class ImprovedAerodynamics {
|
|||||||
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
||||||
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
||||||
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
|
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
|
||||||
|
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.ControlMode.class);
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.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();
|
Network.register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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, which is switched on with /aero controls 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 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(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,451 @@
|
|||||||
|
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;
|
||||||
|
// 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
|
||||||
|
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;
|
||||||
|
if (!active) {
|
||||||
|
if (cursorAimMode && player != null && seated(player) && serverHasMod(minecraft)) {
|
||||||
|
activate(minecraft, player);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cursorAimMode) {
|
||||||
|
deactivate(minecraft);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (player == null || !seated(player)) {
|
||||||
|
deactivate(minecraft);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(minecraft, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the server tells the client how its player flies. */
|
||||||
|
public static void setMode(boolean cursorAim) {
|
||||||
|
cursorAimMode = cursorAim;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -218,7 +218,7 @@ public final class GunPipperRenderer {
|
|||||||
return true;
|
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;
|
final int segments = 32;
|
||||||
float inner = radius - thickness;
|
float inner = radius - thickness;
|
||||||
float outer = 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);
|
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 INPUT_RATE_PER_TICK = 0.2;
|
||||||
private static final double VELOCITY_EPSILON = 0.05;
|
private static final double VELOCITY_EPSILON = 0.05;
|
||||||
private static final double ANGULAR_EPSILON = 0.005;
|
private static final double ANGULAR_EPSILON = 0.005;
|
||||||
|
private static final ControlAxis[] MEASURED_AXES = {ControlAxis.PITCH, ControlAxis.YAW, ControlAxis.ROLL};
|
||||||
|
|
||||||
private final SubstepClock clock;
|
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
|
// 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 pitchResponse = new Vector3d();
|
||||||
final Vector3d pitchResponseBuilding = 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();
|
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
||||||
volatile double savedTrim;
|
volatile double savedTrim;
|
||||||
|
|
||||||
|
Vector3d responseBuilding(ControlAxis axis) {
|
||||||
|
return switch (axis) {
|
||||||
|
case YAW -> yawResponseBuilding;
|
||||||
|
case ROLL -> rollResponseBuilding;
|
||||||
|
default -> pitchResponseBuilding;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
NodeState(NodeData data) {
|
NodeState(NodeData data) {
|
||||||
pitch = data.pitch();
|
pitch = data.pitch();
|
||||||
yaw = data.yaw();
|
yaw = data.yaw();
|
||||||
@@ -211,6 +225,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
wingStalled = false;
|
wingStalled = false;
|
||||||
for (NodeState state : nodeStates.values()) {
|
for (NodeState state : nodeStates.values()) {
|
||||||
state.pitchResponseBuilding.zero();
|
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();
|
r[2] = s.z() - centerOfMass.z();
|
||||||
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null;
|
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null;
|
||||||
NodeState state = node != null ? nodeStates.get(node.key()) : 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) {
|
if (improved) {
|
||||||
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
|
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, 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);
|
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
|
// How much a little more input on an axis would change the torque: pitch for auto trim, all three for
|
||||||
if (commit && node != null && node.autoTrim() && s.type().controllable && s.listensToPitch()) {
|
// 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;
|
double delta = 0.05;
|
||||||
probe.clear();
|
probe.clear();
|
||||||
double base = 0.0;
|
|
||||||
if (improved) {
|
if (improved) {
|
||||||
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
|
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
|
||||||
} else {
|
} else {
|
||||||
@@ -245,41 +266,45 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
}
|
}
|
||||||
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
|
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
|
||||||
probe.clear();
|
probe.clear();
|
||||||
double nudged = input(s, node, state, delta);
|
double nudged = input(s, node, state, axis, delta);
|
||||||
if (improved) {
|
if (improved) {
|
||||||
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
|
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
|
||||||
} else {
|
} else {
|
||||||
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
||||||
}
|
}
|
||||||
state.pitchResponseBuilding.add(
|
state.responseBuilding(axis).add(
|
||||||
(probe.totalTorque(0) - before[0]) / delta,
|
(probe.totalTorque(0) - before[0]) / delta,
|
||||||
(probe.totalTorque(1) - before[1]) / delta,
|
(probe.totalTorque(1) - before[1]) / delta,
|
||||||
(probe.totalTorque(2) - before[2]) / delta);
|
(probe.totalTorque(2) - before[2]) / delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (commit) {
|
if (commit) {
|
||||||
for (NodeState state : nodeStates.values()) {
|
for (NodeState state : nodeStates.values()) {
|
||||||
state.pitchResponse.set(state.pitchResponseBuilding);
|
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. */
|
/** 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) {
|
if (node == null || state == null) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
double value = axisValue(s.axis1(), node, state, extraPitch) * s.multiplier1()
|
double value = axisValue(s.axis1(), node, state, nudged, extra) * s.multiplier1()
|
||||||
+ axisValue(s.axis2(), node, state, extraPitch) * s.multiplier2();
|
+ axisValue(s.axis2(), node, state, nudged, extra) * s.multiplier2();
|
||||||
return Math.max(-1.0, Math.min(1.0, value));
|
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) {
|
return switch (axis) {
|
||||||
case PITCH -> state.pitch + state.trimOutput + extraPitch;
|
case PITCH -> state.pitch + state.trimOutput + nudge;
|
||||||
case YAW -> state.yaw;
|
case YAW -> state.yaw + nudge;
|
||||||
case ROLL -> state.roll;
|
case ROLL -> state.roll + nudge;
|
||||||
case THROTTLE -> node.throttle();
|
case THROTTLE -> node.throttle();
|
||||||
case NONE -> 0.0;
|
case NONE -> 0.0;
|
||||||
};
|
};
|
||||||
@@ -293,10 +318,12 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
if (state == null) {
|
if (state == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
state.pitch = moveTowards(state.pitch, node.pitch(), step);
|
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys
|
||||||
state.yaw = moveTowards(state.yaw, node.yaw(), step);
|
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled);
|
||||||
state.roll = moveTowards(state.roll, node.roll(), step);
|
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
||||||
AutoTrim.update(state, node, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
|
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() {
|
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,45 @@
|
|||||||
|
package dev.lumareth.improvedAerodynamics.flight;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.network.ControlModePacket;
|
||||||
|
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
import net.minecraftforge.network.PacketDistributor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a player flies: with the keyboard (Warium's own seat controls) or with cursor aim. Chosen per player with
|
||||||
|
* {@code /aero controls}, kept in the player's persistent data and told to their client, which does the rest.
|
||||||
|
*/
|
||||||
|
public final class ControlMode {
|
||||||
|
private static final String KEY = "improved_aerodynamics_cursor_aim";
|
||||||
|
|
||||||
|
private ControlMode() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCursorAim(ServerPlayer player) {
|
||||||
|
CompoundTag persisted = player.getPersistentData().getCompound(Player.PERSISTED_NBT_TAG);
|
||||||
|
return persisted.getBoolean(KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void set(ServerPlayer player, boolean cursorAim) {
|
||||||
|
CompoundTag data = player.getPersistentData();
|
||||||
|
CompoundTag persisted = data.getCompound(Player.PERSISTED_NBT_TAG);
|
||||||
|
persisted.putBoolean(KEY, cursorAim);
|
||||||
|
data.put(Player.PERSISTED_NBT_TAG, persisted);
|
||||||
|
sync(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sync(ServerPlayer player) {
|
||||||
|
Network.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), new ControlModePacket(isCursorAim(player)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onLogin(PlayerEvent.PlayerLoggedInEvent event) {
|
||||||
|
if (event.getEntity() instanceof ServerPlayer player) {
|
||||||
|
sync(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,34 @@
|
|||||||
|
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) {
|
||||||
|
// Only a player who chose cursor aim may set or clear the cursor of a node
|
||||||
|
if (!ControlMode.isCursorAim(player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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,144 @@
|
|||||||
|
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()));
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package dev.lumareth.improvedAerodynamics.network;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.client.aim.CursorAimClient;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** Tells a client whether its player flies with cursor aim, see {@code /aero controls}. */
|
||||||
|
public final class ControlModePacket {
|
||||||
|
private final boolean cursorAim;
|
||||||
|
|
||||||
|
public ControlModePacket(boolean cursorAim) {
|
||||||
|
this.cursorAim = cursorAim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void encode(FriendlyByteBuf buffer) {
|
||||||
|
buffer.writeBoolean(cursorAim);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ControlModePacket decode(FriendlyByteBuf buffer) {
|
||||||
|
return new ControlModePacket(buffer.readBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handle(Supplier<NetworkEvent.Context> context) {
|
||||||
|
boolean value = cursorAim;
|
||||||
|
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> CursorAimClient.setMode(value));
|
||||||
|
context.get().setPacketHandled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,5 +23,16 @@ public final class Network {
|
|||||||
.decoder(ToggleAutoTrimPacket::decode)
|
.decoder(ToggleAutoTrimPacket::decode)
|
||||||
.consumerMainThread(ToggleAutoTrimPacket::handle)
|
.consumerMainThread(ToggleAutoTrimPacket::handle)
|
||||||
.add();
|
.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();
|
||||||
|
CHANNEL.messageBuilder(ControlModePacket.class, 51, NetworkDirection.PLAY_TO_CLIENT)
|
||||||
|
.encoder(ControlModePacket::encode)
|
||||||
|
.decoder(ControlModePacket::decode)
|
||||||
|
.consumerMainThread(ControlModePacket::handle)
|
||||||
|
.add();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,5 +12,10 @@
|
|||||||
"config.improved_aerodynamics.supersonicDrag": "Multiplikator für den Nullauftriebswiderstand bei vollem Überschall, ab Mach 1,3",
|
"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.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.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.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,10 @@
|
|||||||
"config.improved_aerodynamics.supersonicDrag": "Zero-lift drag multiplier once fully supersonic, from Mach 1.3",
|
"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.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.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.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"
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user