Choose cursor aim with /aero controls instead of a keybind

/aero controls keyboard|cursor_aim is open to every player, stored per player
and synced to the client, which switches on automatically when seated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
Claude committed 2026-11-11 18:29:27 +01:00
1 parent d66e8a83d2
commit 2f4f90c773
10 files changed
+133 -20

No files matched your search

@@ -7,6 +7,7 @@ import com.mojang.brigadier.arguments.StringArgumentType;
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.exhaust.NozzleCommands; import dev.lumareth.improvedAerodynamics.exhaust.NozzleCommands;
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.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
@@ -21,7 +22,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 {
@@ -73,21 +75,47 @@ 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(SeedgenCommands.node()); root.then(SeedgenCommands.node().requires(ConfigCommands::mayUse));
root.then(NozzleCommands.node()); root.then(NozzleCommands.node().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 -> {
@@ -27,6 +27,7 @@ public class ImprovedAerodynamics {
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(ExhaustSync.class); MinecraftForge.EVENT_BUS.register(ExhaustSync.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, Config.CLIENT_SPEC); ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
// Own file name: the default one is taken by Config.CLIENT_SPEC // Own file name: the default one is taken by Config.CLIENT_SPEC
@@ -10,10 +10,8 @@ import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import org.lwjgl.glfw.GLFW; 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. */ /** 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 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", 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"); KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_LEFT_ALT, "key.categories.improved_aerodynamics");
@@ -27,7 +25,6 @@ public final class AimKeys {
@SubscribeEvent @SubscribeEvent
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) { public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
event.register(TOGGLE);
event.register(FREE_LOOK); event.register(FREE_LOOK);
} }
} }
@@ -64,6 +64,8 @@ public final class CursorAimClient {
private static final double FREE_LOOK_RETURN_RATE = 10.0; private static final double FREE_LOOK_RETURN_RATE = 10.0;
private static boolean active; private static boolean active;
// Chosen with /aero controls, told by the server
private static boolean cursorAimMode;
// Cursor as components along the craft's forward (gun reticle), right and up axes // Cursor as components along the craft's forward (gun reticle), right and up axes
private static double cf = 1.0; private static double cf = 1.0;
private static double cr; private static double cr;
@@ -93,14 +95,14 @@ public final class CursorAimClient {
} }
Minecraft minecraft = Minecraft.getInstance(); Minecraft minecraft = Minecraft.getInstance();
LocalPlayer player = minecraft.player; LocalPlayer player = minecraft.player;
while (AimKeys.TOGGLE.consumeClick()) { if (!active) {
if (active) { if (cursorAimMode && player != null && seated(player) && serverHasMod(minecraft)) {
deactivate(minecraft);
} else if (player != null) {
activate(minecraft, player); activate(minecraft, player);
} }
return;
} }
if (!active) { if (!cursorAimMode) {
deactivate(minecraft);
return; return;
} }
if (player == null || !seated(player)) { if (player == null || !seated(player)) {
@@ -110,6 +112,11 @@ public final class CursorAimClient {
send(minecraft, true); 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) { private static boolean seated(LocalPlayer player) {
return ModList.get().isLoaded("valkyrien_warium") && AimSeat.seatOf(player) != null; return ModList.get().isLoaded("valkyrien_warium") && AimSeat.seatOf(player) != null;
} }
@@ -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);
}
}
}
@@ -20,7 +20,7 @@ public final class CursorAimServer {
} }
long nodeKey = basis.nodePos().asLong(); long nodeKey = basis.nodePos().asLong();
double length = Math.sqrt(cf * cf + cr * cr + cu * cu); double length = Math.sqrt(cf * cf + cr * cr + cu * cu);
if (!active || !CursorAimConfig.ENABLED.get() || !(length > 1.0E-6) || !Double.isFinite(length)) { if (!active || !ControlMode.isCursorAim(player) || !CursorAimConfig.ENABLED.get() || !(length > 1.0E-6) || !Double.isFinite(length)) {
CursorAimRegistry.clear(nodeKey); CursorAimRegistry.clear(nodeKey);
return; return;
} }
@@ -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);
}
}
@@ -39,5 +39,10 @@ public final class Network {
.decoder(AimPacket::decode) .decoder(AimPacket::decode)
.consumerMainThread(AimPacket::handle) .consumerMainThread(AimPacket::handle)
.add(); .add();
CHANNEL.messageBuilder(ControlModePacket.class, 51, NetworkDirection.PLAY_TO_CLIENT)
.encoder(ControlModePacket::encode)
.decoder(ControlModePacket::decode)
.consumerMainThread(ControlModePacket::handle)
.add();
} }
} }
@@ -23,7 +23,6 @@
"message.improved_aerodynamics.afterburner.throttle": "Der Nachbrenner braucht mindestens Schub %s", "message.improved_aerodynamics.afterburner.throttle": "Der Nachbrenner braucht mindestens Schub %s",
"message.improved_aerodynamics.afterburner.no_seat": "Setz dich in einen Warium-Steuersitz, um den Nachbrenner zu nutzen", "message.improved_aerodynamics.afterburner.no_seat": "Setz dich in einen Warium-Steuersitz, um den Nachbrenner zu nutzen",
"message.improved_aerodynamics.afterburner.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden", "message.improved_aerodynamics.afterburner.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden",
"key.improved_aerodynamics.toggle_cursor_aim": "Cursor-Steuerung umschalten",
"key.improved_aerodynamics.cursor_aim_free_look": "Cursor-Steuerung: Freie Sicht (halten)", "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.on": "Cursor-Steuerung an",
"message.improved_aerodynamics.cursor_aim.off": "Cursor-Steuerung aus", "message.improved_aerodynamics.cursor_aim.off": "Cursor-Steuerung aus",
@@ -23,7 +23,6 @@
"message.improved_aerodynamics.afterburner.throttle": "The afterburner needs a throttle of at least %s", "message.improved_aerodynamics.afterburner.throttle": "The afterburner needs a throttle of at least %s",
"message.improved_aerodynamics.afterburner.no_seat": "Sit in a Warium control seat to use the afterburner", "message.improved_aerodynamics.afterburner.no_seat": "Sit in a Warium control seat to use the afterburner",
"message.improved_aerodynamics.afterburner.no_node": "This seat is not linked to a vehicle control node", "message.improved_aerodynamics.afterburner.no_node": "This seat is not linked to a vehicle control node",
"key.improved_aerodynamics.toggle_cursor_aim": "Toggle cursor aim",
"key.improved_aerodynamics.cursor_aim_free_look": "Cursor aim: free look (hold)", "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.on": "Cursor aim on",
"message.improved_aerodynamics.cursor_aim.off": "Cursor aim off", "message.improved_aerodynamics.cursor_aim.off": "Cursor aim off",