feat(flight): add aspect ratio lift and altitude-hold trim

- Derive lift slope and induced drag from each wing's aspect ratio, found by grouping connected wing blocks across short span gaps.
- Give stabilizers and vertical wings their own parasitic drag from the stabilizerDrag config.
- Replace designSpeed with a fixed lift constant and a liftScale multiplier.
- Make auto trim hold the altitude the craft has when the pilot releases pitch, using nested altitude, climb and attitude loops.
- Add /improvedaerodynamics (alias /aero) to list, read and set the main config values in game.
This commit is contained in:
richard committed 2026-10-28 22:06:50 +01:00
1 parent 4b7fb0e1e8
commit 599ecae21f
10 files changed
+329 -48

No files matched your search

@@ -0,0 +1,91 @@
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.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
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.
* 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) {
}
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<>("trim_authority", Config.TRIM_AUTHORITY, 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<>("lookahead_seconds", Config.LOOKAHEAD_SECONDS, DoubleArgumentType.doubleArg(), Double.class));
private ConfigCommands() {
}
@SubscribeEvent
public static void onRegisterCommands(RegisterCommandsEvent event) {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics")
.requires(source -> source.hasPermission(2))
.executes(context -> list(context.getSource()));
for (Option<?> option : OPTIONS) {
root.then(optionNode(option));
}
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
event.getDispatcher().register(Commands.literal("aero")
.requires(source -> source.hasPermission(2))
.executes(context -> list(context.getSource()))
.redirect(node));
}
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()), false);
return 1;
})
.then(Commands.argument("value", option.argument())
.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()
: "Invalid value for " + option.name()));
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();
}
}