Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
165 lines
11 KiB
Java
165 lines
11 KiB
Java
package dev.lumareth.improvedAerodynamics;
|
|
|
|
import com.mojang.brigadier.arguments.ArgumentType;
|
|
import com.mojang.brigadier.arguments.BoolArgumentType;
|
|
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
|
import dev.lumareth.improvedAerodynamics.exhaust.NozzleCommands;
|
|
import dev.lumareth.improvedAerodynamics.flight.ControlMode;
|
|
import net.minecraft.commands.CommandSourceStack;
|
|
import net.minecraft.commands.Commands;
|
|
import net.minecraft.commands.SharedSuggestionProvider;
|
|
import net.minecraft.network.chat.Component;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraftforge.common.ForgeConfigSpec;
|
|
import net.minecraftforge.event.RegisterCommandsEvent;
|
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
|
|
|
import java.util.List;
|
|
|
|
/**
|
|
* /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.
|
|
* 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.
|
|
*/
|
|
public final class ConfigCommands {
|
|
private record Option<T>(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type,
|
|
List<String> choices) {
|
|
Option(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type) {
|
|
this(name, value, argument, type, List.of());
|
|
}
|
|
}
|
|
|
|
private static final List<Option<?>> OPTIONS = List.of(
|
|
new Option<>("realistic_flight_model", Config.IMPROVED_FLIGHT_MODEL, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("smooth_force_delivery", Config.SMOOTH_FORCE_DELIVERY, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("physics_rate_aerodynamics", Config.PHYSICS_RATE_AERODYNAMICS, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("control_smoothing", Config.CONTROL_SMOOTHING, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("implicit_damping", Config.IMPLICIT_DAMPING, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("lift_scale", Config.LIFT_SCALE, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("stall_angle_scale", Config.STALL_ANGLE_SCALE, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("stabilizer_drag", Config.STABILIZER_DRAG, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("transonic_drag", Config.TRANSONIC_DRAG, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("speed_of_sound", Config.SPEED_OF_SOUND, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("transonic_peak_drag", Config.TRANSONIC_PEAK_DRAG, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("supersonic_drag", Config.SUPERSONIC_DRAG, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("vortex_lift", Config.VORTEX_LIFT, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("vortex_stall_bonus", Config.VORTEX_STALL_BONUS, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("vortex_lift_strength", Config.VORTEX_LIFT_STRENGTH, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flight_computer_authority", Config.FBW_AUTHORITY, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flight_computer_rate_gain", Config.FBW_RATE_GAIN, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flight_computer_integral_gain", Config.FBW_INTEGRAL_GAIN, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flight_computer_stick_rate_degrees", Config.FBW_STICK_RATE_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flight_computer_actuator_rate", Config.FBW_ACTUATOR_RATE, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("trim_min_speed", Config.TRIM_MIN_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("air_placeholder", Config.ENABLE_AIR_PLACEHOLDER, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("nozzle_max_deflection", Config.NOZZLE_MAX_DEFLECTION, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("nozzle_slew_rate", Config.NOZZLE_SLEW_RATE, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("afterburner_min_throttle", Config.AFTERBURNER_MIN_THROTTLE, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("exhaust_style", Config.EXHAUST_STYLE, StringArgumentType.word(), String.class, List.of("wariumvs", "improved")),
|
|
new Option<>("nozzle_yaw_mix", Config.NOZZLE_YAW_MIX, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("nozzle_light", Config.NOZZLE_LIGHT, BoolArgumentType.bool(), Boolean.class),
|
|
new Option<>("flap_takeoff_degrees", Config.FLAP_TAKEOFF_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flap_landing_degrees", Config.FLAP_LANDING_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flap_slew_degrees", Config.FLAP_SLEW_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flap_takeoff_max_speed", Config.FLAP_TAKEOFF_MAX_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("flap_landing_max_speed", Config.FLAP_LANDING_MAX_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
|
new Option<>("lookahead_seconds", Config.LOOKAHEAD_SECONDS, DoubleArgumentType.doubleArg(), Double.class));
|
|
|
|
private ConfigCommands() {
|
|
}
|
|
|
|
/** Operators, and the owner of a singleplayer world even when cheats are off. */
|
|
private static boolean mayUse(CommandSourceStack source) {
|
|
if (source.hasPermission(2)) {
|
|
return true;
|
|
}
|
|
return source.getEntity() instanceof ServerPlayer player
|
|
&& source.getServer().isSingleplayerOwner(player.getGameProfile());
|
|
}
|
|
|
|
@SubscribeEvent
|
|
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")
|
|
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource()));
|
|
for (Option<?> option : OPTIONS) {
|
|
root.then(optionNode(option).requires(ConfigCommands::mayUse));
|
|
}
|
|
root.then(SeedgenCommands.node().requires(ConfigCommands::mayUse));
|
|
root.then(NozzleCommands.node().requires(ConfigCommands::mayUse));
|
|
root.then(controlsNode());
|
|
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
|
|
event.getDispatcher().register(Commands.literal("aero")
|
|
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource()))
|
|
.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) {
|
|
return Commands.literal(option.name())
|
|
.executes(context -> {
|
|
context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get()
|
|
+ (option.choices().isEmpty() ? "" : " (" + String.join(" or ", option.choices()) + ")")), false);
|
|
return 1;
|
|
})
|
|
.then(Commands.argument("value", option.argument())
|
|
.suggests((context, builder) -> SharedSuggestionProvider.suggest(option.choices(), builder))
|
|
.executes(context -> set(context.getSource(), option, context.getArgument("value", option.type()))));
|
|
}
|
|
|
|
private static <T> int set(CommandSourceStack source, Option<T> option, T newValue) {
|
|
ForgeConfigSpec.ValueSpec spec = Config.SPEC.getSpec().get(option.value().getPath());
|
|
if (!spec.test(newValue)) {
|
|
ForgeConfigSpec.Range<?> range = spec.getRange();
|
|
source.sendFailure(Component.literal(range != null
|
|
? option.name() + " must be between " + range.getMin() + " and " + range.getMax()
|
|
: option.choices().isEmpty() ? "Invalid value for " + option.name()
|
|
: option.name() + " must be " + String.join(" or ", option.choices())));
|
|
return 0;
|
|
}
|
|
option.value().set(newValue);
|
|
option.value().save();
|
|
source.sendSuccess(() -> Component.literal(option.name() + " set to " + newValue), true);
|
|
return 1;
|
|
}
|
|
|
|
private static int list(CommandSourceStack source) {
|
|
StringBuilder text = new StringBuilder("Improved Aerodynamics settings:");
|
|
for (Option<?> option : OPTIONS) {
|
|
text.append("\n ").append(option.name()).append(" = ").append(option.value().get());
|
|
}
|
|
source.sendSuccess(() -> Component.literal(text.toString()), false);
|
|
return OPTIONS.size();
|
|
}
|
|
}
|