Author SHA1 Message Date
Claude 6a8a7ead04 feat(client): keep the lead pipper through radar loss
Follows the ship matched to the selected radar contact (true position and
velocity) and extrapolates the last contact when no ship is loaded, so
notching or the radar speed gate no longer removes the pipper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
2026-11-11 12:20:48 +01:00
Claude 9e7aae5773 feat(client): gun pipper with ground impact point and radar lead marker
Draws where the shells of the first light autocannon come down and, for the
selected radar target, a lead pipper. Uses the same ballistics as Warium's
LACFireScript (muzzle speed, barrels, drag 0.99, gravity 0.05).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
2026-11-11 12:02:47 +01:00
80 changed files with 120 additions and 4013 deletions

No files matched your search

+2 -12
View File
@@ -58,13 +58,6 @@ repositories {
includeGroup "maven.modrinth" includeGroup "maven.modrinth"
} }
} }
maven {
name = "AzureDoom Mods"
url = "https://maven.azuredoom.com/mods"
content {
includeGroup "mod.azure.azurelib"
}
}
// Warium VS is not on Modrinth, it is compiled against the jar in libs/ // Warium VS is not on Modrinth, it is compiled against the jar in libs/
flatDir { flatDir {
dir 'libs' dir 'libs'
@@ -80,12 +73,9 @@ dependencies {
compileOnly files('libs/vs-core-api-1.1.0.jar', 'libs/vs-core-util-1.1.0.jar', 'libs/joml-primitives-1.10.0.jar') compileOnly files('libs/vs-core-api-1.1.0.jar', 'libs/vs-core-util-1.1.0.jar', 'libs/joml-primitives-1.10.0.jar')
compileOnly 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22' compileOnly 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
// Warium VS 1.1.7 and Futurism 0.0.4 (optional dependencies, the flight physics mixins only apply when they are present) // Warium VS 1.1.6 and Futurism 0.0.4 (optional dependencies, the flight physics mixins only apply when they are present)
compileOnly fg.deobf("blank:WariumVS:1.1.7") compileOnly fg.deobf("blank:WariumVS:1.1.6")
compileOnly fg.deobf("blank:futurism:0.0.4") compileOnly fg.deobf("blank:futurism:0.0.4")
// AzureLib 3.2.7 for the animated block models (optional dependency, the common jar uses obfuscated names, so it is remapped)
compileOnly fg.deobf("mod.azure.azurelib:azurelib-common-${minecraft_version}:${azurelib_version}")
} }
+1 -2
View File
@@ -12,8 +12,7 @@ mapping_version=2023.09.03-1.20.1
mod_id=improved_aerodynamics mod_id=improved_aerodynamics
mod_name=VS: Improved Aerodynamics mod_name=VS: Improved Aerodynamics
mod_license=MIT mod_license=MIT
mod_version=0.4.0 mod_version=0.3.0
azurelib_version=3.2.7
mod_group_id=dev.lumareth.improvedaerodynamics mod_group_id=dev.lumareth.improvedaerodynamics
mod_authors=Lumareth mod_authors=Lumareth
mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships and smooths out Warium flight physics, with an optional flight model with real stalls and auto trim. mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships and smooths out Warium flight physics, with an optional flight model with real stalls and auto trim.
Binary file not shown.
@@ -123,105 +123,20 @@ public final class Config {
.comment("Improved flight model: multiplier for the extra lift the vortices make at high angles of attack") .comment("Improved flight model: multiplier for the extra lift the vortices make at high angles of attack")
.defineInRange("vortexLiftStrength", 1.0D, 0.0D, 3.0D); .defineInRange("vortexLiftStrength", 1.0D, 0.0D, 3.0D);
public static final ForgeConfigSpec.DoubleValue FBW_AUTHORITY = BUILDER public static final ForgeConfigSpec.DoubleValue TRIM_AUTHORITY = BUILDER
.comment("Flight computer (auto trim): largest total pitch input, as a fraction of full deflection, the computer may use.", .comment("Auto trim: largest pitch input (fraction of full deflection) the trim may hold")
"A craft with strongly relaxed stability needs close to 1") .defineInRange("trimAuthority", 0.5D, 0.0D, 1.0D);
.defineInRange("flightComputerAuthority", 1.0D, 0.0D, 1.0D);
public static final ForgeConfigSpec.DoubleValue FBW_RATE_GAIN = BUILDER
.comment("Flight computer: how hard it corrects a pitch rate that differs from the command, in rad/s² of angular",
"acceleration per rad/s of error. Higher holds the nose tighter, too high makes slow surfaces oscillate")
.defineInRange("flightComputerRateGain", 8.0D, 0.5D, 40.0D);
public static final ForgeConfigSpec.DoubleValue FBW_INTEGRAL_GAIN = BUILDER
.comment("Flight computer: integral gain that removes the steady error left by the torque model")
.defineInRange("flightComputerIntegralGain", 4.0D, 0.0D, 40.0D);
public static final ForgeConfigSpec.DoubleValue FBW_STICK_RATE_DEGREES = BUILDER
.comment("Flight computer: pitch rate in degrees per second the pilot commands with full pitch input")
.defineInRange("flightComputerStickRateDegrees", 40.0D, 5.0D, 120.0D);
public static final ForgeConfigSpec.DoubleValue FBW_ACTUATOR_RATE = BUILDER
.comment("Flight computer: fastest the computer moves the pitch surfaces, in fraction of full deflection per second.",
"Unstable craft need it high enough to catch a divergence, 4 or more")
.defineInRange("flightComputerActuatorRate", 6.0D, 0.5D, 50.0D);
public static final ForgeConfigSpec.DoubleValue TRIM_MIN_SPEED = BUILDER public static final ForgeConfigSpec.DoubleValue TRIM_MIN_SPEED = BUILDER
.comment("Flight computer (auto trim): below this airspeed (blocks per second) the computer holds its current output") .comment("Auto trim: below this airspeed (blocks per second) the trim holds its current value")
.defineInRange("trimMinSpeed", 10.0D, 0.0D, 100.0D); .defineInRange("trimMinSpeed", 10.0D, 0.0D, 100.0D);
public static final ForgeConfigSpec.DoubleValue FLAP_TAKEOFF_DEGREES = BUILDER
.comment("Flaps: deflection in degrees of flaps at the takeoff setting. Flaps need the improved flight model")
.defineInRange("flapTakeoffDegrees", 15.0D, 0.0D, 60.0D);
public static final ForgeConfigSpec.DoubleValue FLAP_LANDING_DEGREES = BUILDER
.comment("Flaps: deflection in degrees of flaps at the landing setting")
.defineInRange("flapLandingDegrees", 35.0D, 0.0D, 60.0D);
public static final ForgeConfigSpec.DoubleValue FLAP_SLEW_DEGREES = BUILDER
.comment("Flaps: how fast they move, in degrees per second")
.defineInRange("flapSlewDegrees", 10.0D, 1.0D, 90.0D);
public static final ForgeConfigSpec.DoubleValue FLAP_TAKEOFF_MAX_SPEED = BUILDER
.comment("Flaps: airspeed in blocks per second above which the air pushes takeoff flaps back in, fully retracted at 25% more")
.defineInRange("flapTakeoffMaxSpeed", 45.0D, 1.0D, 1000.0D);
public static final ForgeConfigSpec.DoubleValue FLAP_LANDING_MAX_SPEED = BUILDER
.comment("Flaps: the same for the landing setting")
.defineInRange("flapLandingMaxSpeed", 35.0D, 1.0D, 1000.0D);
static {
BUILDER.pop();
BUILDER.comment("Vectored afterburner nozzle block").push("nozzle");
}
public static final ForgeConfigSpec.DoubleValue NOZZLE_MAX_DEFLECTION = BUILDER
.comment("Largest angle in degrees the nozzle can swing to either side")
.defineInRange("maxDeflectionDegrees", 20.0D, 1.0D, 45.0D);
public static final ForgeConfigSpec.DoubleValue NOZZLE_SLEW_RATE = BUILDER
.comment("Fastest the nozzle swings, in degrees per second, so it never snaps to a new angle")
.defineInRange("slewRateDegreesPerSecond", 90.0D, 5.0D, 720.0D);
public static final ForgeConfigSpec.DoubleValue AFTERBURNER_MIN_THROTTLE = BUILDER
.comment("Lowest throttle value on the vehicle control node (0 to 10) at which the afterburner key lights the",
"afterburner. It goes out by itself when the throttle falls below it")
.defineInRange("afterburnerMinThrottle", 8.0D, 0.0D, 10.0D);
public static final ForgeConfigSpec.ConfigValue<String> EXHAUST_STYLE = BUILDER
.comment("How every jet exhaust looks: Warium's jet exhaust and VTOL exhaust, Futurism's vectored afterburner and",
"this mod's nozzle. wariumvs for Warium's own effect entities, improved for the big flame, particles and",
"afterburner sound of this mod. Warium's engine sound is kept in both")
.defineInList("exhaustStyle", "improved", java.util.List.of("wariumvs", "improved"));
public static final ForgeConfigSpec.DoubleValue NOZZLE_YAW_MIX = BUILDER
.comment("How much of the yaw input goes into the nozzles' differential deflection. The nozzles are canted, so",
"deflecting them against each other pushes the tail sideways as well as rolling the craft, like on an",
"Su-57. 0 turns yaw vectoring off, a negative value turns it around")
.defineInRange("yawMix", 0.6D, -1.0D, 1.0D);
public static final ForgeConfigSpec.BooleanValue NOZZLE_LIGHT = BUILDER
.comment("The nozzle gives off light while its afterburner is lit")
.define("lightEnabled", true);
static { static {
BUILDER.pop(); BUILDER.pop();
} }
static final ForgeConfigSpec SPEC = BUILDER.build(); static final ForgeConfigSpec SPEC = BUILDER.build();
private static final ForgeConfigSpec.Builder CLIENT_BUILDER = new ForgeConfigSpec.Builder();
public static final ForgeConfigSpec.BooleanValue NOZZLE_PARTICLES = CLIENT_BUILDER
.comment("Jet exhaust in the improved style: smoke at dry thrust and fire in afterburner")
.define("particlesEnabled", true);
public static final ForgeConfigSpec.BooleanValue NOZZLE_SOUND = CLIENT_BUILDER
.comment("Vectored afterburner nozzle: afterburner ignition and heavy afterburner sound")
.define("soundEnabled", true);
static final ForgeConfigSpec CLIENT_SPEC = CLIENT_BUILDER.build();
private Config() { private Config() {
} }
} }
@@ -3,14 +3,10 @@ package dev.lumareth.improvedAerodynamics;
import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.DoubleArgumentType; import com.mojang.brigadier.arguments.DoubleArgumentType;
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.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.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.common.ForgeConfigSpec; import net.minecraftforge.common.ForgeConfigSpec;
@@ -22,16 +18,11 @@ 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.
* The config options need operator rights, except for the owner of a singleplayer world, who may use them without cheats. * Needs 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 {
private record Option<T>(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type, 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( private static final List<Option<?>> OPTIONS = List.of(
@@ -50,24 +41,9 @@ public final class ConfigCommands {
new Option<>("vortex_lift", Config.VORTEX_LIFT, BoolArgumentType.bool(), Boolean.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_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<>("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<>("trim_authority", Config.TRIM_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<>("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<>("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)); new Option<>("lookahead_seconds", Config.LOOKAHEAD_SECONDS, DoubleArgumentType.doubleArg(), Double.class));
private ConfigCommands() { private ConfigCommands() {
@@ -84,56 +60,26 @@ 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")
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource())); .requires(ConfigCommands::mayUse)
.executes(context -> list(context.getSource()));
for (Option<?> option : OPTIONS) { for (Option<?> option : OPTIONS) {
root.then(optionNode(option).requires(ConfigCommands::mayUse)); root.then(optionNode(option));
} }
root.then(SeedgenCommands.node().requires(ConfigCommands::mayUse));
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")
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource())) .requires(ConfigCommands::mayUse)
.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 -> {
context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get() context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get()), false);
+ (option.choices().isEmpty() ? "" : " (" + String.join(" or ", option.choices()) + ")")), false);
return 1; return 1;
}) })
.then(Commands.argument("value", option.argument()) .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())))); .executes(context -> set(context.getSource(), option, context.getArgument("value", option.type()))));
} }
@@ -143,8 +89,7 @@ public final class ConfigCommands {
ForgeConfigSpec.Range<?> range = spec.getRange(); ForgeConfigSpec.Range<?> range = spec.getRange();
source.sendFailure(Component.literal(range != null source.sendFailure(Component.literal(range != null
? option.name() + " must be between " + range.getMin() + " and " + range.getMax() ? option.name() + " must be between " + range.getMin() + " and " + range.getMax()
: option.choices().isEmpty() ? "Invalid value for " + option.name() : "Invalid value for " + option.name()));
: option.name() + " must be " + String.join(" or ", option.choices())));
return 0; return 0;
} }
option.value().set(newValue); option.value().set(newValue);
@@ -1,11 +1,7 @@
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.exhaust.ExhaustSync;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleContent;
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;
@@ -13,7 +9,6 @@ import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger; import org.slf4j.Logger;
@Mod(ImprovedAerodynamics.MOD_ID) @Mod(ImprovedAerodynamics.MOD_ID)
@@ -26,16 +21,8 @@ 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(ExhaustSync.class);
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.ControlMode.class);
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.FlapInteraction.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, PipperConfig.SPEC);
// Own file name: the default one is taken by Config.CLIENT_SPEC
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC, "improved_aerodynamics-pipper-client.toml");
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");
NozzleContent.register(FMLJavaModLoadingContext.get().getModEventBus());
Network.register(); Network.register();
} }
} }
@@ -1,95 +0,0 @@
package dev.lumareth.improvedAerodynamics;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import dev.lumareth.improvedAerodynamics.terrain.AirCeiling;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import java.util.OptionalInt;
/**
* {@code /aero seedgen}: measures where the air starts in one real chunk, for worlds where the air placeholder's
* noise based surface estimate does not work (flat worlds and the like).
* {@code set <x> <y> <z>} picks the chunk containing that position in the current dimension, {@code start} reads its
* blocks and saves the height above which only air remains, {@code clear} forgets it again.
* Pick a chunk that is as high as the tallest terrain you want ships to ignore, and has no builds in it.
*/
final class SeedgenCommands {
private record Selection(ResourceKey<Level> dimension, ChunkPos chunk) {
}
private static Selection selection;
private SeedgenCommands() {
}
static LiteralArgumentBuilder<CommandSourceStack> node() {
return Commands.literal("seedgen")
.executes(context -> status(context.getSource()))
.then(Commands.literal("set")
.then(Commands.argument("pos", BlockPosArgument.blockPos())
.executes(context -> set(context.getSource(), BlockPosArgument.getBlockPos(context, "pos")))))
.then(Commands.literal("start").executes(context -> start(context.getSource())))
.then(Commands.literal("clear").executes(context -> clear(context.getSource())));
}
private static int set(CommandSourceStack source, BlockPos pos) {
ChunkPos chunk = new ChunkPos(pos);
selection = new Selection(source.getLevel().dimension(), chunk);
source.sendSuccess(() -> Component.literal("Chunk " + chunk.x + " " + chunk.z + " in "
+ source.getLevel().dimension().location() + " selected, run /aero seedgen start to analyze it"), true);
return 1;
}
private static int start(CommandSourceStack source) {
if (selection == null) {
source.sendFailure(Component.literal("No chunk selected, use /aero seedgen set <x> <y> <z> first"));
return 0;
}
ServerLevel level = source.getServer().getLevel(selection.dimension());
if (level == null) {
source.sendFailure(Component.literal("Dimension " + selection.dimension().location() + " is not loaded"));
return 0;
}
ChunkPos chunk = selection.chunk();
AirCeiling.Analysis analysis = AirCeiling.analyze(level, chunk);
if (!analysis.complete()) {
source.sendFailure(Component.literal("Chunk " + chunk.x + " " + chunk.z + " still has blocks at y="
+ analysis.highestSolid() + ", the top of what is read (" + AirCeiling.SCAN_MIN_Y + " to "
+ AirCeiling.SCAN_MAX_Y + "), so the air may start higher. Pick a different chunk"));
return 0;
}
AirCeiling.get(level).set(level.dimension(), analysis.airHeight());
source.sendSuccess(() -> Component.literal("Only air above y=" + analysis.highestSolid() + " in chunk " + chunk.x + " "
+ chunk.z + ", saved y=" + analysis.airHeight() + " for " + level.dimension().location()
+ ". The air placeholder hands VS air from there up" + (Config.ENABLE_AIR_PLACEHOLDER.get() ? ""
: " (turn it on with /aero air_placeholder true)")), true);
return 1;
}
private static int clear(CommandSourceStack source) {
ServerLevel level = source.getLevel();
boolean removed = AirCeiling.get(level).clear(level.dimension());
source.sendSuccess(() -> Component.literal(removed ? "Measured air height of " + level.dimension().location()
+ " removed, the placeholder estimates it from world generation again"
: "No measured air height for " + level.dimension().location()), true);
return removed ? 1 : 0;
}
private static int status(CommandSourceStack source) {
ServerLevel level = source.getLevel();
OptionalInt height = AirCeiling.get(level).ceiling(level.dimension());
String selected = selection != null ? "chunk " + selection.chunk().x + " " + selection.chunk().z + " in "
+ selection.dimension().location() : "none";
source.sendSuccess(() -> Component.literal("Selected: " + selected + "\nMeasured air height in "
+ level.dimension().location() + ": " + (height.isPresent() ? "y=" + height.getAsInt() : "none, estimated from world generation")), false);
return 1;
}
}
@@ -2,9 +2,7 @@ package dev.lumareth.improvedAerodynamics.client;
import com.mojang.blaze3d.platform.InputConstants; import com.mojang.blaze3d.platform.InputConstants;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics; import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import dev.lumareth.improvedAerodynamics.network.FlapPacket;
import dev.lumareth.improvedAerodynamics.network.Network; import dev.lumareth.improvedAerodynamics.network.Network;
import dev.lumareth.improvedAerodynamics.network.ToggleAfterburnerPacket;
import dev.lumareth.improvedAerodynamics.network.ToggleAutoTrimPacket; import dev.lumareth.improvedAerodynamics.network.ToggleAutoTrimPacket;
import net.minecraft.client.KeyMapping; import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
@@ -20,15 +18,6 @@ public final class KeyBindings {
public static final KeyMapping TOGGLE_AUTO_TRIM = new KeyMapping("key.improved_aerodynamics.toggle_auto_trim", public static final KeyMapping TOGGLE_AUTO_TRIM = new KeyMapping("key.improved_aerodynamics.toggle_auto_trim",
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, "key.categories.improved_aerodynamics"); KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, "key.categories.improved_aerodynamics");
public static final KeyMapping TOGGLE_AFTERBURNER = new KeyMapping("key.improved_aerodynamics.toggle_afterburner",
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_K, "key.categories.improved_aerodynamics");
public static final KeyMapping FLAPS_DOWN = new KeyMapping("key.improved_aerodynamics.flaps_down",
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_G, "key.categories.improved_aerodynamics");
public static final KeyMapping FLAPS_UP = new KeyMapping("key.improved_aerodynamics.flaps_up",
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_H, "key.categories.improved_aerodynamics");
private KeyBindings() { private KeyBindings() {
} }
@@ -40,9 +29,6 @@ public final class KeyBindings {
@SubscribeEvent @SubscribeEvent
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) { public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
event.register(TOGGLE_AUTO_TRIM); event.register(TOGGLE_AUTO_TRIM);
event.register(TOGGLE_AFTERBURNER);
event.register(FLAPS_DOWN);
event.register(FLAPS_UP);
} }
} }
@@ -64,25 +50,6 @@ public final class KeyBindings {
Network.CHANNEL.sendToServer(new ToggleAutoTrimPacket()); Network.CHANNEL.sendToServer(new ToggleAutoTrimPacket());
} }
} }
while (TOGGLE_AFTERBURNER.consumeClick()) {
if (minecraft.player != null && minecraft.player.getVehicle() != null && minecraft.getConnection() != null
&& Network.CHANNEL.isRemotePresent(minecraft.getConnection().getConnection())) {
Network.CHANNEL.sendToServer(new ToggleAfterburnerPacket());
}
}
while (FLAPS_DOWN.consumeClick()) {
sendFlaps(minecraft, true);
}
while (FLAPS_UP.consumeClick()) {
sendFlaps(minecraft, false);
}
}
private static void sendFlaps(Minecraft minecraft, boolean extend) {
if (minecraft.player != null && minecraft.player.getVehicle() != null && minecraft.getConnection() != null
&& Network.CHANNEL.isRemotePresent(minecraft.getConnection().getConnection())) {
Network.CHANNEL.sendToServer(new FlapPacket(extend));
}
} }
} }
} }
@@ -1,31 +0,0 @@
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);
}
}
}
@@ -1,15 +0,0 @@
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;
}
}
@@ -1,451 +0,0 @@
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);
}
}
@@ -1,45 +0,0 @@
package dev.lumareth.improvedAerodynamics.client.aim;
import net.minecraftforge.common.ForgeConfigSpec;
/** Client side settings of cursor aim. Lives in improved_aerodynamics-cursor-aim-client.toml. */
public final class CursorAimClientConfig {
public static final ForgeConfigSpec SPEC;
public static final ForgeConfigSpec.DoubleValue LOOK_AHEAD;
public static final ForgeConfigSpec.DoubleValue MOUSE_SENSITIVITY;
public static final ForgeConfigSpec.DoubleValue SPRING_BACK_SECONDS;
public static final ForgeConfigSpec.DoubleValue MAX_CURSOR_ANGLE_DEGREES;
public static final ForgeConfigSpec.DoubleValue ZOOM_STEP;
public static final ForgeConfigSpec.DoubleValue ZOOM_MAX;
public static final ForgeConfigSpec.DoubleValue MARKER_SIZE;
public static final ForgeConfigSpec.BooleanValue SHOW_RETICLE;
static {
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
builder.comment("Cursor aim, client side").push("cursor_aim");
LOOK_AHEAD = builder.comment("How strongly the camera looks into the direction of the turn. 0 never moves it, 1 (100%) turns it quickly",
"to look where the cursor points. In between it follows more slowly")
.defineInRange("lookAhead", 0.0, 0.0, 1.0);
MOUSE_SENSITIVITY = builder.comment("Multiplier for how far the mouse moves the cursor (or the camera while the free look key is held)")
.defineInRange("mouseSensitivity", 1.0, 0.05, 10.0);
SPRING_BACK_SECONDS = builder.comment("The cursor returns to the gun reticle with this time constant in seconds when you stop moving the mouse.",
"0 makes it stay where you left it")
.defineInRange("springBackSeconds", 1.0, 0.0, 10.0);
MAX_CURSOR_ANGLE_DEGREES = builder.comment("Furthest the cursor can go from the gun reticle, 135 is a 270 degree cone")
.defineInRange("maxCursorAngleDegrees", 135.0, 10.0, 180.0);
ZOOM_STEP = builder.comment("Blocks the camera moves per mouse wheel notch")
.defineInRange("zoomStep", 2.0, 0.1, 20.0);
ZOOM_MAX = builder.comment("Furthest the camera can be pulled back from its normal distance, in blocks")
.defineInRange("zoomMax", 80.0, 0.0, 500.0);
MARKER_SIZE = builder.comment("Radius of the cursor ring on screen")
.defineInRange("markerSize", 0.04, 0.01, 0.3);
SHOW_RETICLE = builder.comment("Draw a small cross where the gun reticle points, the zero of the cursor, so you can see it when the camera looks away")
.define("showReticle", true);
builder.pop();
SPEC = builder.build();
}
private CursorAimClientConfig() {
}
}
@@ -218,7 +218,7 @@ public final class GunPipperRenderer {
return true; return true;
} }
public static void ring(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float radius, float thickness, int color) { private 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 {
} }
} }
public static void quad(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float halfX, float halfY, int color) { private 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);
@@ -1,48 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import dev.lumareth.improvedAerodynamics.Config;
import dev.lumareth.improvedAerodynamics.flight.AutoTrimToggle;
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.entity.BlockEntity;
/**
* Server side of the afterburner key. The switch lives in the control node, like auto trim, so it stays with the
* craft and every nozzle linked to the node follows it. It only lights with enough throttle and goes out for good
* once the throttle falls below that, see {@link NozzleBlockEntity}.
*/
public final class AfterburnerToggle {
/** Tag on the control node that holds the switch. */
public static final String TAG = "improved_aerodynamics_afterburner";
private AfterburnerToggle() {
}
public static void toggle(ServerPlayer player) {
Entity vehicle = player.getVehicle();
if (!(vehicle instanceof SeatEntityEntity)) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.afterburner.no_seat"), true);
return;
}
BlockEntity node = AutoTrimToggle.findNode(vehicle.level(), vehicle.blockPosition());
if (node == null) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.afterburner.no_node"), true);
return;
}
CompoundTag tag = node.getPersistentData();
double minimum = Config.AFTERBURNER_MIN_THROTTLE.get();
if (!tag.getBoolean(TAG) && tag.getDouble("Throttle") < minimum) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.afterburner.throttle", (int) Math.ceil(minimum)), true);
return;
}
boolean enabled = !tag.getBoolean(TAG);
tag.putBoolean(TAG, enabled);
node.setChanged();
player.displayClientMessage(Component.translatable(enabled
? "message.improved_aerodynamics.afterburner.on" : "message.improved_aerodynamics.afterburner.off"), true);
}
}
@@ -1,77 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import dev.lumareth.improvedAerodynamics.network.ExhaustPacket;
import dev.lumareth.improvedAerodynamics.network.Network;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.server.ServerStoppedEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.network.PacketDistributor;
import org.joml.Vector3d;
import org.valkyrienskies.mod.common.VSGameUtilsKt;
import java.util.HashMap;
import java.util.Map;
/**
* Tells the players near a burning jet exhaust about it, for the improved exhaust style. A packet goes out when the
* exhaust changes and otherwise every few ticks, which keeps the flame alive on the clients, see {@code ExhaustFlames}.
*/
public final class ExhaustSync {
private static final int RESEND_TICKS = 4;
private static final double RANGE = 256.0;
private static boolean announced;
private record Sent(long tick, Vec3 direction, int intensity, boolean afterburner) {
}
private static final Map<ResourceKey<Level>, Long2ObjectOpenHashMap<Sent>> SENT = new HashMap<>();
private ExhaustSync() {
}
/**
* @param key the block the exhaust belongs to, one flame per key
* @param exit where the flame starts, in the block's coordinates
* @param direction unit vector the exhaust moves along
* @param intensity 0 to 1
*/
public static void emit(ServerLevel level, BlockPos key, Vec3 exit, Vec3 direction, float intensity, boolean afterburner) {
Long2ObjectOpenHashMap<Sent> sent = SENT.computeIfAbsent(level.dimension(), dimension -> new Long2ObjectOpenHashMap<>());
long now = level.getGameTime();
int quantized = Math.round(Math.max(0.0F, Math.min(1.0F, intensity)) * 100.0F);
Sent last = sent.get(key.asLong());
if (last != null && now - last.tick() < RESEND_TICKS && last.intensity() == quantized && last.afterburner() == afterburner
&& last.direction().distanceToSqr(direction) < 1.0E-4) {
return;
}
sent.put(key.asLong(), new Sent(now, direction, quantized, afterburner));
ExhaustPacket packet = new ExhaustPacket(key, exit.subtract(key.getX(), key.getY(), key.getZ()), direction, quantized, afterburner);
// Near where the exhaust is in the world: on a ship the block itself is far out in the ship's own chunks, which
// nobody is tracking in the usual way
Vec3 world = exit;
var ship = VSGameUtilsKt.getShipManagingPos(level, key);
if (ship != null) {
Vector3d transformed = ship.getShipToWorld().transformPosition(new Vector3d(exit.x, exit.y, exit.z));
world = new Vec3(transformed.x, transformed.y, transformed.z);
}
Vec3 point = world;
Network.CHANNEL.send(PacketDistributor.NEAR.with(() -> new PacketDistributor.TargetPoint(point.x, point.y, point.z,
RANGE, level.dimension())), packet);
if (!announced) {
announced = true;
ImprovedAerodynamics.LOGGER.info("Sent the first improved exhaust flame, at {} (world {}) to the players within {} blocks",
key.toShortString(), point, RANGE);
}
}
@SubscribeEvent
public static void onServerStopped(ServerStoppedEvent event) {
SENT.clear();
}
}
@@ -1,46 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import net.mcreator.valkyrienwarium.ships.ForceData;
import net.mcreator.valkyrienwarium.ships.ForceDirectionMode;
import net.mcreator.valkyrienwarium.ships.ForceInducedShips;
import net.mcreator.valkyrienwarium.ships.ForceMode;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.Vec3;
import org.joml.Vector3d;
import org.valkyrienskies.core.api.ships.LoadedServerShip;
import org.valkyrienskies.core.api.ships.ServerShip;
import org.valkyrienskies.mod.common.VSGameUtilsKt;
/**
* Thrust of a nozzle, the way Futurism's vectored afterburner makes it: the power comes from the Warium jet exhaust
* directly behind the nozzle, which hands it over through its {@code ExhaustPower} value instead of pushing the ship
* itself because the nozzle is in the {@code warium_vs:blocksjet} tag. The push goes opposite to the way the exhaust
* leaves, so a nozzle deflected upwards pushes the tail down. Only used where Warium VS is installed.
*/
final class ExhaustThrust {
private ExhaustThrust() {
}
/** The power the jet exhaust directly behind the nozzle hands over, 0 if there is none or it is off. */
static double power(Level level, BlockPos pos, Direction facing) {
BlockEntity exhaust = level.getBlockEntity(pos.relative(facing.getOpposite()));
return exhaust != null ? exhaust.getPersistentData().getDouble("ExhaustPower") : 0.0;
}
static void apply(ServerLevel level, BlockPos pos, Direction facing, Vec3 exhaustDirection) {
double power = power(level, pos, facing);
if (power <= 0.0) {
return;
}
LoadedServerShip ship = VSGameUtilsKt.getLoadedShipManagingPos(level, pos);
if (ship == null || ((ServerShip) ship).isStatic()) {
return;
}
Vector3d push = new Vector3d(-exhaustDirection.x, -exhaustDirection.y, -exhaustDirection.z);
ForceInducedShips.getOrCreate(ship).addForce(pos, new ForceData(push, power, ForceMode.POSITION, ForceDirectionMode.SHIP));
}
}
@@ -1,116 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import dev.lumareth.improvedAerodynamics.Config;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.Block;
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;
import net.minecraft.world.phys.Vec3;
/**
* The improved exhaust style, see {@code exhaustStyle} in the config. It applies to every block that makes a jet exhaust:
* Warium's jet exhaust and VTOL exhaust, Futurism's vectored afterburner behind which a jet exhaust sits, and this mod's
* nozzle. In the other style Warium's own effect entities draw the flame and none of this runs.
* <p>
* Warium's exhausts do not spawn their effect entities then. When one would have, it sends a burning flame to the clients instead, from the
* block in front if that one takes over the thrust (the {@code blocksjet} tag), at its exit and along its deflection. The
* engine behind the exhaust says how hard it works ({@code Power}) and whether its afterburner burns. Warium's sound is
* kept.
*/
public final class ImprovedExhaust {
private static final double FULL_POWER = 100.0;
private static final TagKey<Block> TAKES_OVER_THRUST = TagKey.create(Registries.BLOCK, new ResourceLocation("warium_vs", "blocksjet"));
private ImprovedExhaust() {
}
public static boolean isImproved() {
try {
return "improved".equals(Config.EXHAUST_STYLE.get());
} catch (IllegalStateException e) {
// The config is not loaded yet
return true;
}
}
// What the exhaust being ticked would have spawned, it only ticks on the server thread and one at a time
private static boolean jetEffect;
private static boolean afterburnerEffect;
/** Called when an exhaust starts its tick. */
public static void begin() {
jetEffect = false;
afterburnerEffect = false;
}
/**
* Used instead of adding Warium's effect entity. The exhaust only makes one when it burns, so that is the signal to
* send the improved flame; in the improved style the entity itself is not added, in the other it is, as always.
*/
public static boolean addEffectEntity(ServerLevel level, Entity entity) {
if (!isImproved()) {
return level.addFreshEntity(entity);
}
var key = net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(entity.getType());
if (key != null && key.getPath().contains("after")) {
afterburnerEffect = true;
} else {
jetEffect = true;
}
return false;
}
/** Sends the flame of the exhaust at this position for this tick. */
public static void plume(LevelAccessor accessor, double x, double y, double z, BlockState state) {
Direction facing = facingOf(state);
if (!isImproved() || !(accessor instanceof ServerLevel level) || facing == null) {
return;
}
if (!jetEffect && !afterburnerEffect) {
return;
}
BlockPos pos = BlockPos.containing(x, y, z);
boolean afterburner = afterburnerEffect;
// How hard it works, if the engine behind says so, otherwise a flame that is clearly there
BlockEntity engine = level.getBlockEntity(pos.relative(facing.getOpposite()));
double power = engine != null ? engine.getPersistentData().getDouble("Power") : 0.0;
float intensity = power > 0.0 ? (float) Math.min(1.0, 0.3 + power / FULL_POWER) : (afterburner ? 1.0F : 0.8F);
Vec3 exit = NozzleGeometry.exit(pos, facing);
Vec3 direction = new Vec3(facing.getStepX(), facing.getStepY(), facing.getStepZ());
BlockPos frontPos = pos.relative(facing);
BlockState front = level.getBlockState(frontPos);
if (front.is(TAKES_OVER_THRUST)) {
if (front.getBlock() instanceof NozzleBlock) {
// The nozzle draws its own flame, swinging with its nozzle
return;
}
// Futurism's vectored afterburner: its flame leaves its exit, tilted by its vector value
Direction frontFacing = facingOf(front) != null ? facingOf(front) : facing;
BlockEntity frontEntity = level.getBlockEntity(frontPos);
double tilt = frontEntity != null ? frontEntity.getPersistentData().getDouble("Vectorpitch") : 0.0;
exit = NozzleGeometry.exit(frontPos, frontFacing);
direction = new Vec3(frontFacing.getStepX(), frontFacing.getStepY() + tilt, frontFacing.getStepZ()).normalize();
}
ExhaustSync.emit(level, pos, exit, direction, intensity, afterburner);
}
/** The direction a Warium block faces, from its property called facing, which is not always the same property. */
public static Direction facingOf(BlockState state) {
for (Property<?> property : state.getProperties()) {
if (property instanceof DirectionProperty direction && property.getName().equals("facing")) {
return state.getValue(direction);
}
}
return null;
}
}
@@ -1,89 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.DirectionalBlock;
import net.minecraft.world.level.block.Mirror;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.phys.BlockHitResult;
import org.jetbrains.annotations.Nullable;
/**
* Jet engine nozzle with thrust vectoring. Faces the way its exhaust leaves, like Warium's jet exhausts, and is drawn by
* AzureLib. Sneak-use with an empty hand switches it between left and right engine.
*/
public class NozzleBlock extends BaseEntityBlock {
public static final BooleanProperty LIT = BooleanProperty.create("lit");
public NozzleBlock() {
super(Properties.of().mapColor(MapColor.METAL).strength(3.0F, 6.0F).requiresCorrectToolForDrops()
.sound(SoundType.METAL).noOcclusion().lightLevel(state -> state.getValue(LIT) ? 13 : 0));
registerDefaultState(stateDefinition.any().setValue(DirectionalBlock.FACING, net.minecraft.core.Direction.NORTH)
.setValue(LIT, false));
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block, BlockState> builder) {
builder.add(DirectionalBlock.FACING, LIT);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(DirectionalBlock.FACING, context.getNearestLookingDirection().getOpposite());
}
@Override
public BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(DirectionalBlock.FACING, rotation.rotate(state.getValue(DirectionalBlock.FACING)));
}
@Override
public BlockState mirror(BlockState state, Mirror mirror) {
return state.rotate(mirror.getRotation(state.getValue(DirectionalBlock.FACING)));
}
@Override
public RenderShape getRenderShape(BlockState state) {
return RenderShape.ENTITYBLOCK_ANIMATED;
}
@Override
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (!player.isShiftKeyDown() || !player.getItemInHand(hand).isEmpty()) {
return InteractionResult.PASS;
}
if (!level.isClientSide && level.getBlockEntity(pos) instanceof NozzleBlockEntity nozzle) {
nozzle.setRightEngine(!nozzle.isRightEngine());
player.displayClientMessage(Component.translatable(nozzle.isRightEngine()
? "message.improved_aerodynamics.nozzle.right" : "message.improved_aerodynamics.nozzle.left"), true);
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
@Nullable
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new NozzleBlockEntity(pos, state);
}
@Nullable
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
return createTickerHelper(type, NozzleContent.NOZZLE_ENTITY.get(), NozzleBlockEntity::tick);
}
}
@@ -1,281 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import dev.lumareth.improvedAerodynamics.Config;
import dev.lumareth.improvedAerodynamics.exhaust.client.NozzleClientEffects;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.DirectionalBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.Nullable;
/**
* State of one vectored afterburner nozzle. The server owns it: whatever drives the nozzle (a debug command now, flight
* logic later) calls {@link #setDeflectionTarget}, {@link #setThrottle} and {@link #setAfterburner}, the rest follows
* from them. Everything the client needs to draw the nozzle is in the update tag, so players that join or reload the
* chunk see the right state.
*/
public class NozzleBlockEntity extends BlockEntity {
// Warium's throttle goes from 0 to 10
private static final float MAX_NODE_THROTTLE = 10.0F;
private static final boolean WARIUM_LOADED = net.minecraftforge.fml.ModList.get().isLoaded("valkyrien_warium");
// What changes are worth an update packet: a quarter degree, two percent throttle
private static final float DEFLECTION_STEP = (float) Math.toRadians(0.25);
private static final float THROTTLE_STEP = 0.02F;
private float deflectionTarget;
private float deflectionPrevious;
private float deflectionCurrent;
private float throttle;
private boolean afterburnerRequested;
private boolean afterburnerAuto;
private boolean afterburner;
private boolean rightEngine;
// Last values sent to the clients
private int sentDeflection;
private int sentThrottle;
private boolean sentAfterburner;
private boolean sentRightEngine;
/** The client's sound and particle state, owned by {@link NozzleClientEffects}. */
@Nullable
private Object clientEffects;
public NozzleBlockEntity(BlockPos pos, BlockState state) {
super(NozzleContent.NOZZLE_ENTITY.get(), pos, state);
}
// ---- API ----
/** Sets where the nozzle should swing to, in radians, positive turns the nozzle's rotation bone positively. */
public void setDeflectionTarget(float radians) {
float limit = (float) Math.toRadians(Config.NOZZLE_MAX_DEFLECTION.get());
deflectionTarget = Math.max(-limit, Math.min(limit, radians));
changed();
}
/** Sets the engine throttle, 0 to 1. Only matters for a nozzle that is not linked to a control node. */
public void setThrottle(float throttle) {
this.throttle = Math.max(0.0F, Math.min(1.0F, throttle));
changed();
}
/** Forces the afterburner on, independent of the control node. Switching it off leaves it to the afterburner key again. */
public void setAfterburner(boolean on) {
afterburnerRequested = on;
changed();
}
public void setRightEngine(boolean right) {
rightEngine = right;
changed();
}
public float getDeflectionTarget() {
return deflectionTarget;
}
/** The deflection being drawn, eased towards the target, interpolated between ticks. */
public float getDeflection(float partialTick) {
return deflectionPrevious + (deflectionCurrent - deflectionPrevious) * partialTick;
}
public float getThrottle() {
return throttle;
}
public boolean isAfterburner() {
return afterburner;
}
public boolean isRightEngine() {
return rightEngine;
}
public Direction getFacing() {
return getBlockState().getValue(DirectionalBlock.FACING);
}
// ---- Ticking ----
public static void tick(Level level, BlockPos pos, BlockState state, NozzleBlockEntity nozzle) {
nozzle.tick(level);
}
private void tick(Level level) {
if (!level.isClientSide) {
drive(level);
}
float maxStep = (float) Math.toRadians(Config.NOZZLE_SLEW_RATE.get() / 20.0);
deflectionPrevious = deflectionCurrent;
deflectionCurrent += Math.max(-maxStep, Math.min(maxStep, deflectionTarget - deflectionCurrent));
if (level.isClientSide) {
NozzleClientEffects.tick(this);
return;
}
boolean lit = afterburnerRequested || afterburnerAuto;
if (lit != afterburner) {
afterburner = lit;
setChanged();
}
sync();
boolean light = lit && Config.NOZZLE_LIGHT.get();
BlockState state = getBlockState();
if (state.getValue(NozzleBlock.LIT) != light) {
level.setBlock(worldPosition, state.setValue(NozzleBlock.LIT, light), 3);
}
if (WARIUM_LOADED && level instanceof ServerLevel serverLevel) {
ExhaustThrust.apply(serverLevel, worldPosition, getFacing(),
NozzleGeometry.exhaustDirection(getFacing(), deflectionCurrent, rightEngine));
}
}
/**
* Takes throttle, deflection and afterburner from the linked control node. A nozzle without a link keeps what the
* API or the debug command gave it.
*/
private void drive(Level level) {
NozzleControl.Inputs inputs = NozzleControl.read(level, this);
if (inputs == null) {
afterburnerAuto = false;
return;
}
// No flame without a running engine behind the nozzle: the jet exhaust there hands over its power
boolean running = !WARIUM_LOADED || ExhaustThrust.power(level, worldPosition, getFacing()) > 0.0;
throttle = running ? Math.min(1.0F, (float) inputs.throttle() / MAX_NODE_THROTTLE) : 0.0F;
float pitch = (float) inputs.pitchUp();
float roll = (float) inputs.rollRight();
deflectionTarget = NozzleMixing.deflection(pitch, roll, (float) inputs.yawRight(), rightEngine);
double minimum = Config.AFTERBURNER_MIN_THROTTLE.get();
if (inputs.afterburnerSwitch() && inputs.throttle() < minimum) {
// Too little throttle: out, and it stays out until the key is pressed again
inputs.node().getPersistentData().putBoolean(AfterburnerToggle.TAG, false);
inputs.node().setChanged();
}
afterburnerAuto = running && inputs.afterburnerSwitch() && inputs.throttle() >= minimum;
}
/** Saves the state, and tells the clients if something they can see changed. */
private void changed() {
setChanged();
sync();
}
private void sync() {
if (level == null || level.isClientSide) {
return;
}
int deflection = Math.round(deflectionTarget / DEFLECTION_STEP);
int throttleStep = Math.round(throttle / THROTTLE_STEP);
if (deflection != sentDeflection || throttleStep != sentThrottle || afterburner != sentAfterburner || rightEngine != sentRightEngine) {
sentDeflection = deflection;
sentThrottle = throttleStep;
sentAfterburner = afterburner;
sentRightEngine = rightEngine;
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), UPDATE_CLIENTS);
}
}
private static final int UPDATE_CLIENTS = 2;
// ---- Saving and syncing ----
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
tag.putFloat("Deflection", deflectionTarget);
tag.putFloat("Throttle", throttle);
tag.putBoolean("Afterburner", afterburner);
tag.putBoolean("AfterburnerRequested", afterburnerRequested);
tag.putBoolean("RightEngine", rightEngine);
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
deflectionTarget = tag.getFloat("Deflection");
throttle = tag.getFloat("Throttle");
afterburner = tag.getBoolean("Afterburner");
afterburnerRequested = tag.getBoolean("AfterburnerRequested");
afterburnerAuto = afterburner && !afterburnerRequested;
rightEngine = tag.getBoolean("RightEngine");
if (level == null || !level.isClientSide || deflectionCurrent == 0.0F && deflectionPrevious == 0.0F) {
// Loaded fresh: start at the target instead of swinging in from zero
deflectionCurrent = deflectionTarget;
deflectionPrevious = deflectionTarget;
}
sentDeflection = Math.round(deflectionTarget / DEFLECTION_STEP);
sentThrottle = Math.round(throttle / THROTTLE_STEP);
sentAfterburner = afterburner;
sentRightEngine = rightEngine;
}
@Override
public CompoundTag getUpdateTag() {
return saveWithoutMetadata();
}
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public void onDataPacket(Connection connection, ClientboundBlockEntityDataPacket packet) {
CompoundTag tag = packet.getTag();
if (tag != null) {
// Warium's key configuration screen reads the keys from the block's persistent data on the client, so that
// has to arrive too. Not load() as a whole: the deflection on screen must keep easing from where it is
super.load(tag);
deflectionTarget = tag.getFloat("Deflection");
throttle = tag.getFloat("Throttle");
afterburner = tag.getBoolean("Afterburner");
rightEngine = tag.getBoolean("RightEngine");
}
}
// ---- Client ----
@Nullable
public Object getClientEffects() {
return clientEffects;
}
public void setClientEffects(@Nullable Object clientEffects) {
this.clientEffects = clientEffects;
}
@Override
public AABB getRenderBoundingBox() {
// The nozzle swings out of the block and the flame trails behind it
return new AABB(worldPosition).inflate(2.0);
}
@Override
public void setRemoved() {
super.setRemoved();
stopEffects();
}
@Override
public void onChunkUnloaded() {
super.onChunkUnloaded();
stopEffects();
}
private void stopEffects() {
if (level != null && level.isClientSide) {
NozzleClientEffects.stop(this);
}
}
}
@@ -1,70 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.FloatArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import java.util.function.Consumer;
/**
* {@code /aero nozzle <pos> ...}: moves a vectored afterburner nozzle by hand, for testing it without the flight system.
* {@code deflection <degrees>}, {@code throttle <0..1>}, {@code afterburner <true|false>} and {@code right <true|false>}.
*/
public final class NozzleCommands {
private NozzleCommands() {
}
public static LiteralArgumentBuilder<CommandSourceStack> node() {
return Commands.literal("nozzle").then(Commands.argument("pos", BlockPosArgument.blockPos())
.executes(context -> status(context.getSource(), BlockPosArgument.getLoadedBlockPos(context, "pos")))
.then(Commands.literal("deflection").then(Commands.argument("degrees", FloatArgumentType.floatArg(-90.0F, 90.0F))
.executes(context -> {
float degrees = FloatArgumentType.getFloat(context, "degrees");
return modify(context.getSource(), BlockPosArgument.getLoadedBlockPos(context, "pos"),
nozzle -> nozzle.setDeflectionTarget((float) Math.toRadians(degrees)));
})))
.then(Commands.literal("throttle").then(Commands.argument("throttle", FloatArgumentType.floatArg(0.0F, 1.0F))
.executes(context -> {
float throttle = FloatArgumentType.getFloat(context, "throttle");
return modify(context.getSource(), BlockPosArgument.getLoadedBlockPos(context, "pos"),
nozzle -> nozzle.setThrottle(throttle));
})))
.then(Commands.literal("afterburner").then(Commands.argument("on", BoolArgumentType.bool())
.executes(context -> {
boolean on = BoolArgumentType.getBool(context, "on");
return modify(context.getSource(), BlockPosArgument.getLoadedBlockPos(context, "pos"),
nozzle -> nozzle.setAfterburner(on));
})))
.then(Commands.literal("right").then(Commands.argument("right", BoolArgumentType.bool())
.executes(context -> {
boolean right = BoolArgumentType.getBool(context, "right");
return modify(context.getSource(), BlockPosArgument.getLoadedBlockPos(context, "pos"),
nozzle -> nozzle.setRightEngine(right));
}))));
}
private static int modify(CommandSourceStack source, BlockPos pos, Consumer<NozzleBlockEntity> change) {
if (!(source.getLevel().getBlockEntity(pos) instanceof NozzleBlockEntity nozzle)) {
source.sendFailure(Component.literal("There is no vectored afterburner nozzle at " + pos.toShortString()));
return 0;
}
change.accept(nozzle);
return status(source, pos);
}
private static int status(CommandSourceStack source, BlockPos pos) {
if (!(source.getLevel().getBlockEntity(pos) instanceof NozzleBlockEntity nozzle)) {
source.sendFailure(Component.literal("There is no vectored afterburner nozzle at " + pos.toShortString()));
return 0;
}
source.sendSuccess(() -> Component.literal(String.format("Nozzle at %s: deflection %.1f deg, throttle %.2f, afterburner %s, %s engine",
pos.toShortString(), Math.toDegrees(nozzle.getDeflectionTarget()), nozzle.getThrottle(),
nozzle.isAfterburner() ? "on" : "off", nozzle.isRightEngine() ? "right" : "left")), false);
return 1;
}
}
@@ -1,46 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
/** Registry entries of the vectored afterburner nozzle. */
public final class NozzleContent {
private static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, ImprovedAerodynamics.MOD_ID);
private static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, ImprovedAerodynamics.MOD_ID);
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
DeferredRegister.create(ForgeRegistries.BLOCK_ENTITY_TYPES, ImprovedAerodynamics.MOD_ID);
private static final DeferredRegister<CreativeModeTab> TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, ImprovedAerodynamics.MOD_ID);
public static final RegistryObject<Block> NOZZLE = BLOCKS.register("vectored_afterburner_nozzle", NozzleBlock::new);
public static final RegistryObject<Item> NOZZLE_ITEM = ITEMS.register("vectored_afterburner_nozzle",
() -> new BlockItem(NOZZLE.get(), new Item.Properties()));
public static final RegistryObject<BlockEntityType<NozzleBlockEntity>> NOZZLE_ENTITY = BLOCK_ENTITIES.register("vectored_afterburner_nozzle",
() -> BlockEntityType.Builder.of(NozzleBlockEntity::new, NOZZLE.get()).build(null));
public static final RegistryObject<CreativeModeTab> TAB = TABS.register("main", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup." + ImprovedAerodynamics.MOD_ID))
.icon(() -> NOZZLE_ITEM.get().getDefaultInstance())
.displayItems((parameters, output) -> output.accept(NOZZLE_ITEM.get()))
.build());
private NozzleContent() {
}
public static void register(IEventBus modBus) {
BLOCKS.register(modBus);
ITEMS.register(modBus);
BLOCK_ENTITIES.register(modBus);
TABS.register(modBus);
}
}
@@ -1,68 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.registries.ForgeRegistries;
import org.jetbrains.annotations.Nullable;
/**
* What a nozzle reads from the vehicle control node it was linked to with Warium's linker, and how its two keys, set
* with Warium's two key configuration screen, are read. Either key can name any axis: Pitch+ means W raises the nose and
* Pitch- means S does, Roll+ or Roll- says which roll input rolls the plane right, Yaw+ or Yaw- which yaw input yaws it
* right. An axis no key names is off, except that a nozzle with no key at all pitches with W, and yaw follows D without
* a key.
*/
final class NozzleControl {
private static final ResourceLocation NODE_BLOCK = new ResourceLocation("valkyrien_warium", "vehicle_control_node");
/** The control node's state in node units: pitch, roll and yaw from -1 to 1, throttle from 0 to 10. Yaw needs no key, D is right. */
record Inputs(BlockEntity node, double pitchUp, double rollRight, double yawRight, double throttle, boolean afterburnerSwitch) {
}
private NozzleControl() {
}
@Nullable
static Inputs read(Level level, NozzleBlockEntity nozzle) {
CompoundTag own = nozzle.getPersistentData();
if (!own.contains("ControlX")) {
return null;
}
BlockPos nodePos = BlockPos.containing(own.getDouble("ControlX"), own.getDouble("ControlY"), own.getDouble("ControlZ"));
if (!level.isLoaded(nodePos) || !ForgeRegistries.BLOCKS.getKey(level.getBlockState(nodePos).getBlock()).equals(NODE_BLOCK)) {
return null;
}
BlockEntity node = level.getBlockEntity(nodePos);
if (node == null) {
return null;
}
CompoundTag tag = node.getPersistentData();
String key = own.getString("Key");
String key2 = own.getString("Key2");
boolean unset = (key.isEmpty() || key.equals("Null")) && (key2.isEmpty() || key2.equals("Null"));
double pitch = clamp(tag.getDouble("Pitch")) * sign(key, key2, "Pitch", unset ? 1.0 : 0.0);
double roll = clamp(tag.getDouble("Roll")) * sign(key, key2, "Roll", 0.0);
double yaw = clamp(tag.getDouble("Yaw")) * sign(key, key2, "Yaw", 1.0);
return new Inputs(node, pitch, roll, yaw, Math.max(0.0, tag.getDouble("Throttle")), tag.getBoolean(AfterburnerToggle.TAG));
}
/** +1 for axis+, -1 for axis-, from the first key that names the axis, the fallback if neither does. */
private static double sign(String key, String key2, String axis, double fallback) {
for (String candidate : new String[]{key, key2}) {
if (candidate.equals(axis + "+")) {
return 1.0;
}
if (candidate.equals(axis + "-")) {
return -1.0;
}
}
return fallback;
}
private static double clamp(double value) {
return Math.max(-1.0, Math.min(1.0, value));
}
}
@@ -1,55 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import net.minecraft.core.Direction;
import net.minecraft.world.phys.Vec3;
import org.joml.Quaternionf;
import org.joml.Vector3f;
/**
* Where the exhaust leaves the nozzle and which way it goes, in the block's own coordinates (ship coordinates on a
* ship). Matches the model: the nozzle sits in the north half of the block, tilted by the hinge bone and swung by the
* rotation bone, and the block is turned by its facing the way AzureLib turns the model.
*/
public final class NozzleGeometry {
/** Static tilt of the hinge bone around the thrust axis, in degrees, see the model. */
private static final float HINGE_TILT = 32.5F;
/** The right engine swings about the mirrored axis, so the same deflection turns the opposite way there. */
public static final float RIGHT_ENGINE_SIGN = -1.0F;
private NozzleGeometry() {
}
/** Center of the nozzle exit, on the block face the exhaust leaves through. */
public static Vec3 exit(net.minecraft.core.BlockPos pos, Direction facing) {
return new Vec3(pos.getX() + 0.5 + facing.getStepX() * 0.5, pos.getY() + 0.5 + facing.getStepY() * 0.5,
pos.getZ() + 0.5 + facing.getStepZ() * 0.5);
}
/**
* Unit vector the exhaust moves along. At no deflection it is the facing, a deflection swings it around the axis the
* rotation bone turns about: the hinge's tilt applied to X for the left engine, to Y for the right one.
*/
public static Vec3 exhaustDirection(Direction facing, float deflection, boolean rightEngine) {
Vector3f swing = new Vector3f(0.0F, 0.0F, -1.0F);
swing.rotateAxis((rightEngine ? RIGHT_ENGINE_SIGN : 1.0F) * deflection, rightEngine ? 0.0F : 1.0F, rightEngine ? 1.0F : 0.0F, 0.0F);
// The deflection is about an axis turned by the hinge, the counter bone turns the nozzle back
swing.rotateZ((float) Math.toRadians(HINGE_TILT));
swing.rotate(facingRotation(facing));
return new Vec3(swing.x, swing.y, swing.z);
}
/** The turn AzureLib applies to a model for a block facing, from facing north. */
private static Quaternionf facingRotation(Direction facing) {
Quaternionf rotation = new Quaternionf();
switch (facing) {
case SOUTH -> rotation.rotateY((float) Math.toRadians(180));
case WEST -> rotation.rotateY((float) Math.toRadians(90));
case EAST -> rotation.rotateY((float) Math.toRadians(270));
case UP -> rotation.rotateX((float) Math.toRadians(90));
case DOWN -> rotation.rotateX((float) Math.toRadians(-90));
default -> {
}
}
return rotation;
}
}
@@ -1,29 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust;
import dev.lumareth.improvedAerodynamics.Config;
/**
* Turns pilot inputs into the deflection of one nozzle of a twin engine craft: the same sign on both engines pitches,
* opposite signs roll and yaw. The nozzles hinge on canted axes, so deflecting them against each other turns the exhaust
* sideways as well as up and down: the vertical parts make a rolling moment, the sideways parts push the tail around.
* That coupling is how an Su-57 yaws with its nozzles, so roll and yaw share the one differential channel. Positive
* deflection always means the exhaust swings up, on both sides. Not tied to any input source, whatever drives the
* nozzles calls this and then {@link NozzleBlockEntity#setDeflectionTarget}.
*/
public final class NozzleMixing {
private NozzleMixing() {
}
/**
* @param pitchUp -1 to 1, positive raises the nose, which takes the exhaust up
* @param rollRight -1 to 1, positive rolls the plane right, which takes the right engine's exhaust up and the left one's down
* @param yawRight -1 to 1, positive yaws the nose right. The left engine's exhaust swings up and the right one's down,
* which for this model's canting turns both sideways to the left and pushes the tail left
* @return target deflection in radians, within the configured limit
*/
public static float deflection(float pitchUp, float rollRight, float yawRight, boolean rightEngine) {
float differential = rollRight - yawRight * Config.NOZZLE_YAW_MIX.get().floatValue();
float mixed = Math.max(-1.0F, Math.min(1.0F, pitchUp + (rightEngine ? differential : -differential)));
return mixed * (float) Math.toRadians(Config.NOZZLE_MAX_DEFLECTION.get());
}
}
@@ -1,144 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.lumareth.improvedAerodynamics.Config;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import dev.lumareth.improvedAerodynamics.exhaust.ImprovedExhaust;
import dev.lumareth.improvedAerodynamics.network.ExhaustPacket;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.level.LevelEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.joml.Matrix4dc;
import org.joml.Quaternionf;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.valkyrienskies.core.api.ships.ClientShip;
import org.valkyrienskies.mod.common.VSGameUtilsKt;
import java.util.Iterator;
/**
* The flames of Warium's jet exhausts in the improved style. The server says which exhausts burn, see {@code ExhaustSync},
* and each stays alive on the client for a few ticks after its last message, fading out. They are drawn after the
* particles, in the world, on ships with the ship's interpolated transform, and give off their particles here.
*/
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
public final class ExhaustFlames {
private static final int LIFETIME_TICKS = 10;
private static final int FADE_TICKS = 4;
private record Flame(BlockPos pos, Vec3 exit, Vec3 direction, float intensity, boolean afterburner, long expires) {
}
private static final Long2ObjectOpenHashMap<Flame> FLAMES = new Long2ObjectOpenHashMap<>();
private static boolean announced;
private ExhaustFlames() {
}
public static void accept(ExhaustPacket packet) {
Level level = Minecraft.getInstance().level;
if (level == null) {
return;
}
if (!announced) {
announced = true;
ImprovedAerodynamics.LOGGER.info("Received the first improved exhaust flame, at {}", packet.pos().toShortString());
}
Vec3 exit = packet.exitOffset().add(packet.pos().getX(), packet.pos().getY(), packet.pos().getZ());
FLAMES.put(packet.pos().asLong(), new Flame(packet.pos(), exit, packet.direction(), packet.intensity() / 100.0F,
packet.afterburner(), level.getGameTime() + LIFETIME_TICKS));
}
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
Level level = Minecraft.getInstance().level;
if (event.phase != TickEvent.Phase.END || level == null || FLAMES.isEmpty()) {
return;
}
if (!ImprovedExhaust.isImproved()) {
FLAMES.clear();
return;
}
boolean particles = Config.NOZZLE_PARTICLES.get();
Iterator<Flame> iterator = FLAMES.values().iterator();
while (iterator.hasNext()) {
Flame flame = iterator.next();
if (level.getGameTime() > flame.expires()) {
iterator.remove();
} else if (particles) {
Vec3 world = transform(level, flame.pos(), flame.exit(), false, 1.0F);
Vec3 direction = transform(level, flame.pos(), flame.direction(), true, 1.0F);
ExhaustParticles.emit(level, world, direction, flame.intensity(), flame.afterburner());
}
}
}
@SubscribeEvent
public static void onRenderLevel(RenderLevelStageEvent event) {
Level level = Minecraft.getInstance().level;
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES || level == null || FLAMES.isEmpty()
|| !ImprovedExhaust.isImproved()) {
return;
}
float partialTick = event.getPartialTick();
Vec3 camera = event.getCamera().getPosition();
MultiBufferSource.BufferSource buffers = Minecraft.getInstance().renderBuffers().bufferSource();
VertexConsumer buffer = buffers.getBuffer(FlameRenderer.RENDER_TYPE);
for (Flame flame : FLAMES.values()) {
// Only afterburners burn with the big flame, dry thrust is smoke
if (!flame.afterburner()) {
continue;
}
float fade = Math.min(1.0F, (flame.expires() - level.getGameTime() + partialTick) / FADE_TICKS);
Vec3 exit = transform(level, flame.pos(), flame.exit(), false, partialTick);
Vec3 direction = transform(level, flame.pos(), flame.direction(), true, partialTick);
PoseStack poseStack = event.getPoseStack();
poseStack.pushPose();
poseStack.translate(exit.x - camera.x, exit.y - camera.y, exit.z - camera.z);
poseStack.mulPose(new Quaternionf().rotationTo(new Vector3f(0.0F, 0.0F, -1.0F),
new Vector3f((float) direction.x, (float) direction.y, (float) direction.z)));
PoseStack.Pose pose = poseStack.last();
FlameRenderer.draw(buffer, pose.pose(), pose.normal(), 0.0F, 0.0F, 0.0F, flame.intensity(),
level.getGameTime() + partialTick, Math.max(0.0F, fade));
poseStack.popPose();
}
buffers.endBatch(FlameRenderer.RENDER_TYPE);
}
@SubscribeEvent
public static void onLevelUnload(LevelEvent.Unload event) {
if (event.getLevel().isClientSide()) {
FLAMES.clear();
}
}
/** A position or direction in the block's coordinates as one in the world, differs on a ship. */
private static Vec3 transform(Level level, BlockPos pos, Vec3 vector, boolean direction, float partialTick) {
var ship = VSGameUtilsKt.getShipManagingPos(level, pos);
if (ship == null) {
return vector;
}
Matrix4dc shipToWorld = ship instanceof ClientShip client && partialTick != 1.0F ? client.getRenderTransform().getShipToWorld()
: ship.getShipToWorld();
Vector3d result = new Vector3d(vector.x, vector.y, vector.z);
if (direction) {
shipToWorld.transformDirection(result);
} else {
shipToWorld.transformPosition(result);
}
return new Vec3(result.x, result.y, result.z);
}
}
@@ -1,44 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
/** The particles of a jet exhaust in the improved style, for the nozzle and for Warium's exhausts alike. */
final class ExhaustParticles {
private ExhaustParticles() {
}
/**
* @param exit where the exhaust leaves, in the world
* @param direction unit vector the exhaust moves along, in the world
* @param intensity 0 to 1
*/
static void emit(Level level, Vec3 exit, Vec3 direction, float intensity, boolean afterburner) {
RandomSource random = level.getRandom();
if (afterburner) {
// A hot plume: fire, a blue core and a heavy smoke trail behind it
for (int i = 0; i < 4; i++) {
spawn(level, random, ParticleTypes.FLAME, exit, direction, 0.8 + 1.4 * intensity, 0.1);
}
for (int i = 0; i < 2; i++) {
spawn(level, random, ParticleTypes.SOUL_FIRE_FLAME, exit, direction, 1.2 + 1.6 * intensity, 0.03);
}
spawn(level, random, ParticleTypes.LARGE_SMOKE, exit, direction, 0.7 + 0.9 * intensity, 0.12);
if (random.nextFloat() < 0.4F) {
spawn(level, random, ParticleTypes.CAMPFIRE_COSY_SMOKE, exit, direction, 0.5, 0.15);
}
} else if (random.nextFloat() < intensity) {
spawn(level, random, ParticleTypes.LARGE_SMOKE, exit, direction, 0.4 + 0.6 * intensity, 0.06);
}
}
private static void spawn(Level level, RandomSource random, ParticleOptions type, Vec3 position, Vec3 direction, double speed, double spread) {
level.addParticle(type, position.x + (random.nextDouble() - 0.5) * 0.3, position.y + (random.nextDouble() - 0.5) * 0.3,
position.z + (random.nextDouble() - 0.5) * 0.3,
direction.x * speed + (random.nextDouble() - 0.5) * spread, direction.y * speed + (random.nextDouble() - 0.5) * spread,
direction.z * speed + (random.nextDouble() - 0.5) * spread);
}
}
@@ -1,90 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.ResourceLocation;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
/**
* The afterburner flame: glowing cones with a dull red halo, an orange body, a yellow middle and a blue-white core,
* shock diamonds along the axis like a real afterburner and a bright glow at the exit. Length and brightness follow the
* throttle and everything flickers. They use the plain white texture and vertex colors, so no model is needed. The flame
* points along -Z from the origin it is drawn at, the caller turns and places it.
*/
final class FlameRenderer {
static final RenderType RENDER_TYPE = RenderType.entityTranslucentEmissive(new ResourceLocation("textures/misc/white.png"));
// Radius at the base, and length range
private static final float BASE_RADIUS = 0.4F;
private static final float MIN_LENGTH = 1.8F;
private static final float LENGTH_RANGE = 4.5F;
private static final int PLANES = 3;
private static final int DIAMONDS = 5;
// Radius factor, length factor, red, green, blue, alpha at the base
private static final float[][] CONES = {
{1.45F, 1.10F, 1.00F, 0.25F, 0.05F, 0.22F},
{1.00F, 1.00F, 1.00F, 0.45F, 0.10F, 0.55F},
{0.70F, 0.82F, 1.00F, 0.75F, 0.30F, 0.75F},
{0.38F, 0.58F, 0.85F, 0.92F, 1.00F, 0.95F},
};
private FlameRenderer() {
}
/**
* @param origin where the flame starts, in the space of the matrix
* @param throttle 0 to 1, how long and bright the flame is
* @param time ticks with partial tick, drives the flicker
* @param fade 0 to 1, extra brightness factor for flames that are dying down
*/
static void draw(VertexConsumer buffer, Matrix4f matrix, Matrix3f normal, float originX, float originY, float originZ,
float throttle, float time, float fade) {
float flicker = 0.9F + 0.07F * (float) Math.sin(time * 1.9F) + 0.05F * (float) Math.sin(time * 4.3F + 1.0F)
+ 0.03F * (float) Math.sin(time * 9.1F);
float length = (MIN_LENGTH + LENGTH_RANGE * throttle) * flicker * (0.4F + 0.6F * fade);
float brightness = (0.55F + 0.45F * throttle) * fade;
Draw draw = new Draw(buffer, matrix, normal, originX, originY);
for (float[] cone : CONES) {
float pulse = 1.0F + 0.06F * (float) Math.sin(time * 5.0F + cone[0] * 7.0F);
draw.cone(originZ, originZ - length * cone[1], BASE_RADIUS * cone[0] * pulse,
cone[2], cone[3], cone[4], Math.min(1.0F, cone[5] * brightness));
}
// Shock diamonds: two cones back to back, spaced along the flame, shrinking and pulsing
for (int i = 0; i < DIAMONDS; i++) {
float center = originZ - length * (0.14F + 0.15F * i);
float radius = BASE_RADIUS * 0.34F * (1.0F - 0.13F * i) * (0.85F + 0.15F * (float) Math.sin(time * 6.0F + i * 1.7F));
float half = length * 0.055F;
float alpha = Math.min(1.0F, 0.95F * brightness * (1.0F - 0.1F * i));
draw.cone(center, center - half, radius, 0.75F, 0.9F, 1.0F, alpha);
draw.cone(center, center + half, radius, 0.75F, 0.9F, 1.0F, alpha);
}
// Bright glow right at the exit
draw.cone(originZ, originZ - 0.35F, BASE_RADIUS * 0.9F, 1.0F, 0.95F, 0.8F, 0.9F * brightness);
}
/** The buffer and placement of one flame, so the cones only need to say where they are. */
private record Draw(VertexConsumer buffer, Matrix4f matrix, Matrix3f normal, float x, float y) {
/** A cone of {@code PLANES} crossed triangles from a base circle at baseZ to a point at tipZ. */
void cone(float baseZ, float tipZ, float radius, float red, float green, float blue, float baseAlpha) {
for (int plane = 0; plane < PLANES; plane++) {
double angle = Math.PI * plane / PLANES;
float dx = (float) Math.cos(angle) * radius;
float dy = (float) Math.sin(angle) * radius;
vertex(x - dx, y - dy, baseZ, red, green, blue, baseAlpha);
vertex(x + dx, y + dy, baseZ, red, green, blue, baseAlpha);
vertex(x, y, tipZ, red, green, blue, 0.0F);
vertex(x, y, tipZ, red, green, blue, 0.0F);
}
}
private void vertex(float vx, float vy, float vz, float red, float green, float blue, float alpha) {
buffer.vertex(matrix, vx, vy, vz).color(red, green, blue, alpha).uv(0.5F, 0.5F)
.overlayCoords(OverlayTexture.NO_OVERLAY).uv2(LightTexture.FULL_BRIGHT).normal(normal, 0.0F, 0.0F, -1.0F).endVertex();
}
}
}
@@ -1,56 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleBlockEntity;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleGeometry;
import mod.azure.azurelib.animation.controller.AzAnimationController;
import mod.azure.azurelib.animation.controller.AzAnimationControllerContainer;
import mod.azure.azurelib.animation.impl.AzBlockAnimator;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
/**
* Swings the nozzle. The model has no keyframes, the animation file is a stub the animator needs anyway; the rotation
* bone is set from code every frame, from its initial pose, so the angle can never drift.
*/
final class NozzleAnimator extends AzBlockAnimator<NozzleBlockEntity> {
static final ResourceLocation ANIMATION = new ResourceLocation(ImprovedAerodynamics.MOD_ID,
"animations/blockentities/nozzle.animation.json");
private static final String ROTATION_BONE = "rotation";
/**
* The left engine's nozzle swings about the model's X axis. The right one about Y, the axis that mirrors the hinge's
* tilt, and turns the opposite way for the same deflection, see {@link NozzleGeometry#RIGHT_ENGINE_SIGN}.
*/
private static final float LEFT_SIGN = 1.0F;
NozzleAnimator() {
super(mod.azure.azurelib.animation.AzAnimatorConfig.defaultConfig());
}
@Override
public void registerControllers(AzAnimationControllerContainer<NozzleBlockEntity> container) {
container.add(AzAnimationController.builder(this, "base_controller").build());
}
@Override
public @NotNull ResourceLocation getAnimationLocation(NozzleBlockEntity animatable) {
return ANIMATION;
}
@Override
public void setCustomAnimations(NozzleBlockEntity nozzle, float partialTicks) {
super.setCustomAnimations(nozzle, partialTicks);
var model = context().boneCache().getBakedModel();
var rotation = model != null ? model.getBoneOrNull(ROTATION_BONE) : null;
if (rotation == null) {
return;
}
float theta = nozzle.getDeflection(partialTicks);
var initial = rotation.getInitialAzSnapshot();
if (nozzle.isRightEngine()) {
rotation.setRotY(initial.getRotY() + NozzleGeometry.RIGHT_ENGINE_SIGN * theta);
} else {
rotation.setRotX(initial.getRotX() + LEFT_SIGN * theta);
}
}
}
@@ -1,113 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import dev.lumareth.improvedAerodynamics.Config;
import dev.lumareth.improvedAerodynamics.exhaust.ImprovedExhaust;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleBlockEntity;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleGeometry;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.registries.ForgeRegistries;
import org.joml.Vector3d;
import org.valkyrienskies.mod.common.VSGameUtilsKt;
/**
* Client side sound and particles of the nozzles. Only ever called on the logical client.
* <p>
* Sound is Warium's own: its jet engines have no looping sound either, they play a short blast again and again. The jet
* exhaust behind the nozzle does that, the nozzle adds the heavy blast for the afterburner. Nothing here runs in the
* wariumvs exhaust style.
*/
public final class NozzleClientEffects {
private static final SoundSource CATEGORY = SoundSource.BLOCKS;
private static final int HEAVY_BLAST_INTERVAL = 14;
/** What one nozzle has done so far on this client, kept on the block entity. */
private static final class State {
boolean wasAfterburner;
}
private NozzleClientEffects() {
}
public static void tick(NozzleBlockEntity nozzle) {
Level level = nozzle.getLevel();
if (level == null) {
return;
}
State state = (State) nozzle.getClientEffects();
if (state == null) {
state = new State();
state.wasAfterburner = nozzle.isAfterburner();
nozzle.setClientEffects(state);
}
if (!ImprovedExhaust.isImproved()) {
// Warium's own effect, which comes from the exhaust behind the nozzle, is wanted
state.wasAfterburner = nozzle.isAfterburner();
return;
}
if (Config.NOZZLE_SOUND.get() && nozzle.isAfterburner()) {
playBlasts(level, nozzle, !state.wasAfterburner);
}
state.wasAfterburner = nozzle.isAfterburner();
if (Config.NOZZLE_PARTICLES.get() && nozzle.getThrottle() > 0.05F) {
emitParticles(level, nozzle);
}
}
/** Nothing keeps running on the client, so there is nothing to stop but the remembered state. */
public static void stop(NozzleBlockEntity nozzle) {
nozzle.setClientEffects(null);
}
private static void playBlasts(Level level, NozzleBlockEntity nozzle, boolean ignition) {
RandomSource random = level.getRandom();
Vec3 exit = NozzleGeometry.exit(nozzle.getBlockPos(), nozzle.getFacing());
Vec3 position = toWorld(level, exit.add(NozzleGeometry.exhaustDirection(nozzle.getFacing(), 0.0F, false).scale(4.0)));
long time = level.getGameTime() + nozzle.getBlockPos().hashCode();
// Warium's jet exhaust behind the nozzle already plays the engine blast, the afterburner adds the heavy one
if (ignition || time % HEAVY_BLAST_INTERVAL == 0) {
play(level, "heavyjetblast", position, ignition ? 30.0F : 18.0F, (float) (0.95 + random.nextDouble() * 0.1));
}
}
/** Plays one of Warium's sounds, nothing if Warium is not installed. */
private static void play(Level level, String name, Vec3 position, float volume, float pitch) {
SoundEvent sound = ForgeRegistries.SOUND_EVENTS.getValue(new ResourceLocation("valkyrien_warium", name));
if (sound != null) {
level.playLocalSound(position.x, position.y, position.z, sound, CATEGORY, volume, pitch, false);
}
}
private static void emitParticles(Level level, NozzleBlockEntity nozzle) {
Vec3 exit = NozzleGeometry.exit(nozzle.getBlockPos(), nozzle.getFacing());
Vec3 direction = NozzleGeometry.exhaustDirection(nozzle.getFacing(), nozzle.getDeflection(1.0F), nozzle.isRightEngine());
ExhaustParticles.emit(level, toWorld(level, exit), toWorldDirection(level, nozzle.getBlockPos(), direction),
nozzle.getThrottle(), nozzle.isAfterburner());
}
/** A position in the block's coordinates as a position in the world, differs on a ship. */
public static Vec3 toWorld(Level level, Vec3 position) {
var ship = VSGameUtilsKt.getShipManagingPos(level, position.x, position.y, position.z);
if (ship == null) {
return position;
}
Vector3d world = ship.getShipToWorld().transformPosition(new Vector3d(position.x, position.y, position.z));
return new Vec3(world.x, world.y, world.z);
}
private static Vec3 toWorldDirection(Level level, BlockPos pos, Vec3 direction) {
var ship = VSGameUtilsKt.getShipManagingPos(level, pos);
if (ship == null) {
return direction;
}
Vector3d world = ship.getShipToWorld().transformDirection(new Vector3d(direction.x, direction.y, direction.z));
return new Vec3(world.x, world.y, world.z);
}
}
@@ -1,25 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleContent;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.common.Mod;
/** Registers the nozzle's AzureLib renderer, if AzureLib is installed. */
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
public final class NozzleClientSetup {
private NozzleClientSetup() {
}
@SubscribeEvent
public static void onRegisterRenderers(EntityRenderersEvent.RegisterRenderers event) {
if (!ModList.get().isLoaded("azurelib")) {
ImprovedAerodynamics.LOGGER.warn("AzureLib is not installed, the vectored afterburner nozzle will be invisible");
return;
}
event.registerBlockEntityRenderer(NozzleContent.NOZZLE_ENTITY.get(), context -> new NozzleRenderer());
}
}
@@ -1,47 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.lumareth.improvedAerodynamics.exhaust.ImprovedExhaust;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleBlockEntity;
import mod.azure.azurelib.model.AzBone;
import mod.azure.azurelib.render.AzRendererPipelineContext;
import mod.azure.azurelib.render.layer.AzRenderLayer;
/**
* The afterburner flame of the nozzle, drawn in the space of the nozzle's last bone so it swings with the nozzle, see
* {@link FlameRenderer}.
*/
final class NozzleFlameLayer implements AzRenderLayer<Long, NozzleBlockEntity> {
private static final String NOZZLE_BONE = "counter";
// The middle of the nozzle exit in the model's own coordinates, in blocks
private static final float EXIT_X = 0.5F;
private static final float EXIT_Y = 0.5F;
private static final float EXIT_Z = 0.0F;
@Override
public void preRender(AzRendererPipelineContext<Long, NozzleBlockEntity> context) {
}
@Override
public void render(AzRendererPipelineContext<Long, NozzleBlockEntity> context) {
}
@Override
public void renderForBone(AzRendererPipelineContext<Long, NozzleBlockEntity> context, AzBone bone) {
NozzleBlockEntity nozzle = context.animatable();
if (!NOZZLE_BONE.equals(bone.getName()) || !nozzle.isAfterburner() || nozzle.getLevel() == null
|| !ImprovedExhaust.isImproved()) {
return;
}
PoseStack.Pose pose = context.poseStack().last();
VertexConsumer buffer = context.multiBufferSource().getBuffer(FlameRenderer.RENDER_TYPE);
FlameRenderer.draw(buffer, pose.pose(), pose.normal(), EXIT_X, EXIT_Y, EXIT_Z, nozzle.getThrottle(),
nozzle.getLevel().getGameTime() + context.partialTick(), 1.0F);
}
@Override
public boolean sortsTranslucency() {
return false;
}
}
@@ -1,24 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleBlockEntity;
import mod.azure.azurelib.render.AzLayerRenderer;
import mod.azure.azurelib.render.block.AzBlockEntityModelRenderer;
import mod.azure.azurelib.render.block.AzBlockEntityRendererPipeline;
import net.minecraft.core.Direction;
/**
* AzureLib puts the middle of its models on the middle of the block, the nozzle model is built in the 0 to 16 range of
* the block, so it is moved back by half a block after the block is turned to its facing.
*/
final class NozzleModelRenderer extends AzBlockEntityModelRenderer<NozzleBlockEntity> {
NozzleModelRenderer(AzBlockEntityRendererPipeline<NozzleBlockEntity> pipeline, AzLayerRenderer<Long, NozzleBlockEntity> layerRenderer) {
super(pipeline, layerRenderer);
}
@Override
protected void rotateBlock(Direction facing, PoseStack poseStack) {
super.rotateBlock(facing, poseStack);
poseStack.translate(-0.5, 0.0, -0.5);
}
}
@@ -1,25 +0,0 @@
package dev.lumareth.improvedAerodynamics.exhaust.client;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import dev.lumareth.improvedAerodynamics.exhaust.NozzleBlockEntity;
import mod.azure.azurelib.render.block.AzBlockEntityRenderer;
import mod.azure.azurelib.render.block.AzBlockEntityRendererConfig;
import mod.azure.azurelib.render.block.AzBlockEntityRendererPipeline;
import net.minecraft.resources.ResourceLocation;
/** Draws the vectored afterburner nozzle with AzureLib. */
final class NozzleRenderer extends AzBlockEntityRenderer<NozzleBlockEntity> {
private static final ResourceLocation MODEL = new ResourceLocation(ImprovedAerodynamics.MOD_ID,
"geo/blockentities/vectored_afterburner_nozzle.geo.json");
private static final ResourceLocation TEXTURE = new ResourceLocation(ImprovedAerodynamics.MOD_ID,
"textures/blockentities/jetexausttexture.png");
NozzleRenderer() {
super(AzBlockEntityRendererConfig.<NozzleBlockEntity>builder(MODEL, TEXTURE)
.setAnimatorProvider(NozzleAnimator::new)
.setModelRenderer((pipeline, layerRenderer) ->
new NozzleModelRenderer((AzBlockEntityRendererPipeline<NozzleBlockEntity>) pipeline, layerRenderer))
.addRenderLayer(new NozzleFlameLayer())
.build());
}
}
@@ -19,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap;
* to; at the end of the tick that set is handed to the physics thread, which computes the forces from the ship's * to; at the end of the tick that set is handed to the physics thread, which computes the forces from the ship's
* motion in each substep until the next tick arrives. * motion in each substep until the next tick arrives.
* <p> * <p>
* The physics side also smooths pilot input, runs the flight computer (auto trim), and integrates damping implicitly so stiff damping * The physics side also smooths pilot input, runs auto trim, and integrates damping implicitly so stiff damping
* cannot overshoot. * cannot overshoot.
*/ */
public final class AeroState implements PhysicsBridge.Publishable { public final class AeroState implements PhysicsBridge.Publishable {
@@ -27,7 +27,6 @@ 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;
@@ -55,38 +54,19 @@ public final class AeroState implements PhysicsBridge.Publishable {
double roll; double roll;
double trimIntegral; double trimIntegral;
double trimOutput; double trimOutput;
// Deflection of this node's flaps in radians, moves towards what the flap lever and airspeed allow
double flapAngle;
// Total pitch input the flight computer applied last substep, pilot and trim together
double computerOutput;
// 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();
roll = data.roll(); roll = data.roll();
trimIntegral = data.autoTrim() ? data.storedTrim() : 0.0; trimIntegral = data.autoTrim() ? data.storedTrim() : 0.0;
trimOutput = data.autoTrim() ? 0.0 : trimIntegral; trimOutput = trimIntegral;
savedTrim = trimIntegral; savedTrim = trimIntegral;
computerOutput = pitch + trimOutput;
flapAngle = Math.toRadians(FlapSetting.of(data.flapSetting()).degrees());
} }
} }
@@ -231,8 +211,6 @@ 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();
} }
} }
@@ -243,11 +221,10 @@ 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, ControlAxis.NONE, 0.0) : 0.0; double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0;
double flap = s.flap() && state != null ? state.flapAngle : 0.0;
if (improved) { if (improved) {
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, flap, weight, tuning, stalled[i], out); boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
if (commit) { if (commit) {
stalled[i] = nowStalled; stalled[i] = nowStalled;
wingStalled |= nowStalled && s.type() == SurfaceType.WING; wingStalled |= nowStalled && s.type() == SurfaceType.WING;
@@ -256,62 +233,53 @@ 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 input on an axis would change the torque: pitch for auto trim, all three for // How much a little more pitch input on this node would change the torque, for auto trim
// cursor aim if (commit && node != null && node.autoTrim() && s.type().controllable && s.listensToPitch()) {
if (commit && node != null && s.type().controllable) { double delta = 0.05;
boolean aimed = CursorAimRegistry.isActive(node.key()); probe.clear();
for (ControlAxis axis : MEASURED_AXES) { double base = 0.0;
if (!s.listensTo(axis) || !(aimed || (axis == ControlAxis.PITCH && node.autoTrim()))) { if (improved) {
continue; ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
} } else {
double delta = 0.05; WariumAero.evaluate(s, v, w, r, input, weight, probe);
probe.clear();
if (improved) {
ImprovedAero.evaluate(s, v, w, r, input, flap, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, input, weight, probe);
}
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
probe.clear();
double nudged = input(s, node, state, axis, delta);
if (improved) {
ImprovedAero.evaluate(s, v, w, r, nudged, flap, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
}
state.responseBuilding(axis).add(
(probe.totalTorque(0) - before[0]) / delta,
(probe.totalTorque(1) - before[1]) / delta,
(probe.totalTorque(2) - before[2]) / delta);
} }
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
probe.clear();
double nudged = input(s, node, state, delta);
if (improved) {
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
}
state.pitchResponseBuilding.add(
(probe.totalTorque(0) - before[0]) / delta,
(probe.totalTorque(1) - before[1]) / 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, ControlAxis nudged, double extra) { private static double input(AeroSurface s, NodeData node, NodeState state, double extraPitch) {
if (node == null || state == null) { if (node == null || state == null) {
return 0.0; return 0.0;
} }
double value = axisValue(s.axis1(), node, state, nudged, extra) * s.multiplier1() double value = axisValue(s.axis1(), node, state, extraPitch) * s.multiplier1()
+ axisValue(s.axis2(), node, state, nudged, extra) * s.multiplier2(); + axisValue(s.axis2(), node, state, extraPitch) * 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, ControlAxis nudged, double extra) { private static double axisValue(ControlAxis axis, NodeData node, NodeState state, double extraPitch) {
double nudge = axis == nudged ? extra : 0.0;
return switch (axis) { return switch (axis) {
case PITCH -> state.pitch + state.trimOutput + nudge; case PITCH -> state.pitch + state.trimOutput + extraPitch;
case YAW -> state.yaw + nudge; case YAW -> state.yaw;
case ROLL -> state.roll + nudge; case ROLL -> state.roll;
case THROTTLE -> node.throttle(); case THROTTLE -> node.throttle();
case NONE -> 0.0; case NONE -> 0.0;
}; };
@@ -320,37 +288,18 @@ public final class AeroState implements PhysicsBridge.Publishable {
private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia, private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia,
double[] w, double dt, boolean improved) { double[] w, double dt, boolean improved) {
double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY; double step = Config.CONTROL_SMOOTHING.get() ? INPUT_RATE_PER_TICK * clock.weight() : Double.POSITIVE_INFINITY;
// Torque of the last substep, in ship axes, which the flight computer separates from its own surfaces' share
double[] torque = {wrench.totalTorque(0), wrench.totalTorque(1), wrench.totalTorque(2)};
for (NodeData node : frame.nodes.values()) { for (NodeData node : frame.nodes.values()) {
NodeState state = nodeStates.get(node.key()); NodeState state = nodeStates.get(node.key());
if (state == null) { if (state == null) {
continue; continue;
} }
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys state.pitch = moveTowards(state.pitch, node.pitch(), step);
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled); state.yaw = moveTowards(state.yaw, node.yaw(), step);
state.pitch = moveTowards(state.pitch, effective.pitch(), step); state.roll = moveTowards(state.roll, node.roll(), step);
state.yaw = moveTowards(state.yaw, effective.yaw(), step); AutoTrim.update(state, node, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
state.roll = moveTowards(state.roll, effective.roll(), step);
updateFlaps(state, node, worldVelocity.length(), dt);
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, torque, dt, improved && wingStalled);
} }
} }
/**
* Moves a node's flaps towards the angle its lever asks for. Above the setting's maximum speed the air pushes them
* back in, so a craft that is too fast for its flaps has them half retracted, and all the way at 25% more.
*/
private static void updateFlaps(NodeState state, NodeData node, double speed, double dt) {
FlapSetting setting = FlapSetting.of(node.flapSetting());
double target = Math.toRadians(setting.degrees());
double limit = setting.maxSpeed();
if (speed > limit) {
target *= Math.max(0.0, 1.0 - (speed - limit) / (0.25 * limit));
}
state.flapAngle = moveTowards(state.flapAngle, target, Math.toRadians(Config.FLAP_SLEW_DEGREES.get()) * dt);
}
static double moveTowards(double value, double target, double maxStep) { static double moveTowards(double value, double target, double maxStep) {
if (Math.abs(target - value) <= maxStep) { if (Math.abs(target - value) <= maxStep) {
return target; return target;
@@ -14,17 +14,16 @@ package dev.lumareth.improvedAerodynamics.flight;
* @param brakeInput air brakes only: Warium's input value, they ignore smoothing and trim * @param brakeInput air brakes only: Warium's input value, they ignore smoothing and trim
* @param aspectRatio span squared over area of the wing the block belongs to, 0 if not known yet * @param aspectRatio span squared over area of the wing the block belongs to, 0 if not known yet
* @param wingGroup whether the wing the block belongs to contains WING blocks, not only control surfaces or stabilizers * @param wingGroup whether the wing the block belongs to contains WING blocks, not only control surfaces or stabilizers
* @param flap whether the block was made a flap with shift-right-click, it then also follows its node's flap setting
*/ */
public record AeroSurface(long key, SurfaceType type, boolean slab, double x, double y, double z, int facingX, int facingZ, public record AeroSurface(long key, SurfaceType type, boolean slab, double x, double y, double z, int facingX, int facingZ,
double pressure, double fluidFactor, double speedMultiplier, double pressure, double fluidFactor, double speedMultiplier,
long node, ControlAxis axis1, double multiplier1, ControlAxis axis2, double multiplier2, long node, ControlAxis axis1, double multiplier1, ControlAxis axis2, double multiplier2,
double brakeInput, double aspectRatio, boolean wingGroup, boolean flap) { double brakeInput, double aspectRatio, boolean wingGroup) {
public static final long NO_NODE = Long.MIN_VALUE; public static final long NO_NODE = Long.MIN_VALUE;
AeroSurface withWingShape(double aspectRatio, boolean wingGroup) { AeroSurface withWingShape(double aspectRatio, boolean wingGroup) {
return new AeroSurface(key, type, slab, x, y, z, facingX, facingZ, pressure, fluidFactor, speedMultiplier, return new AeroSurface(key, type, slab, x, y, z, facingX, facingZ, pressure, fluidFactor, speedMultiplier,
node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio, wingGroup, flap); node, axis1, multiplier1, axis2, multiplier2, brakeInput, aspectRatio, wingGroup);
} }
/** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */ /** Warium's "Direction.getClockWise(Axis.Y)" of the facing, the span direction of a horizontal surface. */
@@ -37,10 +36,6 @@ public record AeroSurface(long key, SurfaceType type, boolean slab, double x, do
} }
boolean listensToPitch() { boolean listensToPitch() {
return listensTo(ControlAxis.PITCH); return node != NO_NODE && (axis1 == ControlAxis.PITCH || axis2 == ControlAxis.PITCH);
}
boolean listensTo(ControlAxis axis) {
return node != NO_NODE && (axis1 == axis || axis2 == axis);
} }
} }
@@ -1,101 +0,0 @@
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};
}
}
@@ -7,28 +7,19 @@ import org.joml.Vector3d;
import org.joml.Vector3dc; import org.joml.Vector3dc;
/** /**
* The flight computer of one control node, switched on and off with the auto trim key. It is a fly-by-wire pitch * Auto trim for the pitch control surfaces of one control node: while the pilot gives no pitch input it holds the
* system: the pilot's pitch input is a pitch rate command, not a surface deflection, and the computer moves the * altitude the craft had when the pilot let go, cancelling whatever pitching moment the craft has (off-center weight,
* surfaces to make the craft follow it, whatever the airframe does on its own. * thrust line, speed changes).
* <p> * <p>
* Every physics substep it measures the pitching torque the craft makes with the pitch surfaces taken out * Three nested loops: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than
* ({@code external}: weight offset, thrust line, and above all the airframe's own stability or instability), and the * {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate, and a PI controller moves the trim to reach
* torque one unit of pitch input makes ({@code response}). It then picks the input that makes the craft's total * that rate. Bounding the attitude matters: in Warium's physics the flight path follows the nose only slowly, so
* pitch torque equal the angular acceleration it wants, {@code rateGain * (rate command - pitch rate)} plus a small * steering the flight path directly with pitch rate keeps rotating the nose until the craft dives.
* integral for what the model gets wrong. That cancels a craft that is statically unstable, or trimmed badly, as
* fast as the surfaces can move, so a jet built with relaxed stability can be flown like a stable one.
* <p> * <p>
* Without pilot input the rate command comes from three nested loops that hold the altitude the craft had when the * Attitude is measured from the ship's own up axis, so it assumes the craft is built level, as Warium craft are. The
* pilot let go: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than * trim's effect is measured every substep (torque per unit of pitch input), which gives the pitch axis and lets the
* {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate. Bounding the attitude matters: in Warium's * inner gains be expressed as angular acceleration, so the same tuning works for any craft, speed and altitude.
* physics the flight path follows the nose only slowly, so steering the flight path directly with pitch rate keeps * The trim holds still while the pilot pitches, when too slow, and when wings stall, so it never fights maneuvers.
* rotating the nose until the craft dives.
* <p>
* Attitude is measured from the ship's own up axis, so it assumes the craft is built level, as Warium craft are.
* Torques are measured per unit of input, so the same tuning works for any craft, speed and altitude. Below the
* minimum speed the computer holds its output, the surfaces have no authority there anyway. Stalled wings cut the
* pilot's rate command and the altitude hold, so the computer only keeps the nose where it is. While cursor aim flies
* the node, that is in charge instead.
*/ */
final class AutoTrim { final class AutoTrim {
// Climb rate (blocks/s) per block of altitude error, and its limit // Climb rate (blocks/s) per block of altitude error, and its limit
@@ -41,16 +32,12 @@ final class AutoTrim {
// Pitch rate (rad/s) per radian of attitude error, and its limit // Pitch rate (rad/s) per radian of attitude error, and its limit
private static final double ATTITUDE_GAIN = 1.0; private static final double ATTITUDE_GAIN = 1.0;
private static final double MAX_PITCH_RATE = 0.15; private static final double MAX_PITCH_RATE = 0.15;
// Slow trim movement when the computer is switched off // Angular acceleration per rad/s of pitch rate error, and integral gain
private static final double PROPORTIONAL_GAIN = 3.0;
private static final double INTEGRAL_GAIN = 1.5;
// Fastest the trim moves, in input per second
private static final double TRIM_RATE = 1.0; private static final double TRIM_RATE = 1.0;
private static final double PILOT_DEADBAND = 0.01; private static final double PILOT_DEADBAND = 0.01;
// The computer assumes the surfaces do this much of what was measured, so a response measured too high (stalled
// surfaces, clamped inputs) makes it command a little too much rather than too little, which stays stable
private static final double RESPONSE_MARGIN = 0.7;
// Pitch rate command while stalled, as a fraction of what the pilot asked
private static final double STALLED_COMMAND = 0.3;
// The integral may make up for this much rate error worth of acceleration
private static final double MAX_ERROR_FOR_INTEGRAL = 0.5;
private AutoTrim() { private AutoTrim() {
} }
@@ -62,83 +49,57 @@ final class AutoTrim {
} }
static void update(AeroState.NodeState state, NodeData node, Vector3dc worldVelocity, double altitude, static void update(AeroState.NodeState state, NodeData node, Vector3dc worldVelocity, double altitude,
Quaterniondc rotation, Matrix3dc inertia, double[] w, double[] torque, double dt, boolean stalled) { Quaterniondc rotation, Matrix3dc inertia, double[] w, double dt, boolean stalled) {
Targets targets = state.trimTargets; Targets targets = state.trimTargets;
boolean aimed = CursorAimRegistry.isActive(node.key());
if (!node.autoTrim()) { if (!node.autoTrim()) {
state.trimIntegral = 0.0; state.trimIntegral = 0.0;
state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt); state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt);
state.savedTrim = 0.0; state.savedTrim = 0.0;
state.computerOutput = state.pitch + state.trimOutput;
targets.altitude = Double.NaN; targets.altitude = Double.NaN;
targets.attitudeIntegral = 0.0; targets.attitudeIntegral = 0.0;
return; return;
} }
double authority = Config.TRIM_AUTHORITY.get();
double response = state.pitchResponse.length(); double response = state.pitchResponse.length();
double speed = worldVelocity.length(); double speed = worldVelocity.length();
if (aimed || speed < Config.TRIM_MIN_SPEED.get() || response < 1.0E-6) { boolean hold = Math.abs(node.pitch()) > PILOT_DEADBAND || speed < Config.TRIM_MIN_SPEED.get() || stalled
// Cursor aim, no airflow or no pitch surfaces: the computer holds what it has || response < 1.0E-6;
targets.altitude = Double.NaN; if (hold || Double.isNaN(targets.altitude)) {
targets.attitudeIntegral = 0.0;
state.computerOutput = state.pitch + state.trimOutput;
state.savedTrim = state.trimIntegral;
return;
}
// Pitch axis in ship axes, oriented so that positive rotation is what more pitch input does
Vector3d axis = state.pitchResponse.div(response, new Vector3d());
double axisInertia = axis.dot(inertia.transform(axis, new Vector3d()));
double pitchRate = axis.x * w[0] + axis.y * w[1] + axis.z * w[2];
// Does rotating about that axis raise the nose? Sign of the vertical part of axis x flight direction
Vector3d worldAxis = rotation.transform(axis, new Vector3d());
Vector3d direction = worldVelocity.normalize(new Vector3d());
double noseUp = Math.signum(worldAxis.cross(direction, new Vector3d()).y);
double attitude = attitude(axis, rotation);
double pilot = node.pitch();
boolean pilotActive = Math.abs(pilot) > PILOT_DEADBAND;
double rateCommand;
if (pilotActive || stalled || noseUp == 0.0 || Double.isNaN(attitude)) {
// Hold whatever altitude the craft is at once the pilot lets go // Hold whatever altitude the craft is at once the pilot lets go
targets.altitude = altitude; targets.altitude = altitude;
targets.attitudeIntegral = 0.0; targets.attitudeIntegral = 0.0;
rateCommand = pilotActive }
? pilot * Math.toRadians(Config.FBW_STICK_RATE_DEGREES.get()) * (stalled ? STALLED_COMMAND : 1.0)
: 0.0; double target = state.trimIntegral;
} else { if (!hold) {
if (Double.isNaN(targets.altitude)) { // Pitch axis in ship axes, oriented so that positive rotation is what more pitch input does
targets.altitude = altitude; Vector3d axis = state.pitchResponse.div(response, new Vector3d());
double axisInertia = axis.dot(inertia.transform(axis, new Vector3d()));
double pitchRate = axis.x * w[0] + axis.y * w[1] + axis.z * w[2];
// Does rotating about that axis raise the nose? Sign of the vertical part of axis x flight direction
Vector3d worldAxis = rotation.transform(axis, new Vector3d());
Vector3d direction = worldVelocity.normalize(new Vector3d());
double noseUp = Math.signum(worldAxis.cross(direction, new Vector3d()).y);
double attitude = attitude(axis, rotation);
if (noseUp == 0.0 || Double.isNaN(attitude)) {
return;
} }
double climbTarget = clamp(ALTITUDE_GAIN * (targets.altitude - altitude), MAX_CLIMB_RATE); double climbTarget = clamp(ALTITUDE_GAIN * (targets.altitude - altitude), MAX_CLIMB_RATE);
double climbError = climbTarget - worldVelocity.y(); double climbError = climbTarget - worldVelocity.y();
targets.attitudeIntegral = clamp(targets.attitudeIntegral + CLIMB_INTEGRAL_GAIN * climbError * dt, MAX_ATTITUDE); targets.attitudeIntegral = clamp(targets.attitudeIntegral + CLIMB_INTEGRAL_GAIN * climbError * dt, MAX_ATTITUDE);
// Nose elevation wanted, turned into rotation about the axis // Nose elevation wanted, turned into rotation about the axis
double attitudeTarget = noseUp * clamp(targets.attitudeIntegral + CLIMB_GAIN * climbError, MAX_ATTITUDE); double attitudeTarget = noseUp * clamp(targets.attitudeIntegral + CLIMB_GAIN * climbError, MAX_ATTITUDE);
rateCommand = clamp(ATTITUDE_GAIN * (attitudeTarget - attitude), MAX_PITCH_RATE); double rateTarget = clamp(ATTITUDE_GAIN * (attitudeTarget - attitude), MAX_PITCH_RATE);
double error = rateTarget - pitchRate;
// Pitch input needed per rad/s² of angular acceleration
double gain = axisInertia / response;
state.trimIntegral = clamp(state.trimIntegral + INTEGRAL_GAIN * error * dt * gain, authority);
target = clamp(state.trimIntegral + PROPORTIONAL_GAIN * error * gain, authority);
} }
state.trimOutput = AeroState.moveTowards(state.trimOutput, target, TRIM_RATE * dt);
// Pitching torque about the axis the pitch surfaces are not making: total of the last substep minus what the
// input of that substep made
double total = axis.x * torque[0] + axis.y * torque[1] + axis.z * torque[2];
double external = total - response * state.computerOutput;
double authority = Config.FBW_AUTHORITY.get();
double rateError = rateCommand - pitchRate;
double acceleration = Config.FBW_RATE_GAIN.get() * rateError + state.trimIntegral;
double wanted = (axisInertia * acceleration - external) / (response * RESPONSE_MARGIN);
boolean saturated = Math.abs(wanted) > authority;
wanted = clamp(wanted, authority);
if (!saturated) {
// Integral of the acceleration, bounded so it cannot run away if the model is far off
double limit = Config.FBW_RATE_GAIN.get() * MAX_ERROR_FOR_INTEGRAL;
state.trimIntegral = clamp(state.trimIntegral + Config.FBW_INTEGRAL_GAIN.get() * rateError * dt, limit);
}
double applied = AeroState.moveTowards(state.computerOutput, wanted, Config.FBW_ACTUATOR_RATE.get() * dt);
state.computerOutput = applied;
// The surfaces get the pilot's smoothed input plus the trim, so the trim is what makes up the difference
state.trimOutput = applied - state.pitch;
state.savedTrim = state.trimIntegral; state.savedTrim = state.trimIntegral;
} }
@@ -46,7 +46,7 @@ public final class AutoTrimToggle {
* The control node the seat under the player is linked to. Warium's control seat block finds its seat entity * The control node the seat under the player is linked to. Warium's control seat block finds its seat entity
* within half a block of itself, so the seat block is at the entity's block or just below it. * within half a block of itself, so the seat block is at the entity's block or just below it.
*/ */
public static BlockEntity findNode(Level level, BlockPos seatPos) { private static BlockEntity findNode(Level level, BlockPos seatPos) {
for (BlockPos pos : new BlockPos[]{seatPos, seatPos.below()}) { for (BlockPos pos : new BlockPos[]{seatPos, seatPos.below()}) {
BlockEntity seat = level.getBlockEntity(pos); BlockEntity seat = level.getBlockEntity(pos);
if (seat == null || !seat.getPersistentData().contains("ControlX")) { if (seat == null || !seat.getPersistentData().contains("ControlX")) {
@@ -1,45 +0,0 @@
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);
}
}
}
@@ -1,56 +0,0 @@
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() {
}
}
@@ -1,44 +0,0 @@
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;
}
}
@@ -1,34 +0,0 @@
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()));
}
}
@@ -1,144 +0,0 @@
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(), node.flapSetting());
}
/** 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));
}
}
@@ -1,37 +0,0 @@
package dev.lumareth.improvedAerodynamics.flight;
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.entity.BlockEntity;
/** Server side of the flap keys: moves the flap lever of the control node the pilot's seat is linked to. */
public final class FlapControl {
private FlapControl() {
}
/** @param step +1 for more flaps, -1 for fewer */
public static void step(ServerPlayer player, int step) {
Entity vehicle = player.getVehicle();
if (!(vehicle instanceof SeatEntityEntity)) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps.no_seat"), true);
return;
}
BlockEntity node = AutoTrimToggle.findNode(vehicle.level(), vehicle.blockPosition());
if (node == null) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps.no_node"), true);
return;
}
CompoundTag tag = node.getPersistentData();
int current = FlapSetting.of(tag.getInt(WariumCapture.FLAP_SETTING_TAG)).ordinal();
int next = Math.max(0, Math.min(FlapSetting.values().length - 1, current + (step > 0 ? 1 : -1)));
if (next != current) {
tag.putInt(WariumCapture.FLAP_SETTING_TAG, next);
node.setChanged();
}
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flaps",
Component.translatable(FlapSetting.of(next).translationKey())), true);
}
}
@@ -1,66 +0,0 @@
package dev.lumareth.improvedAerodynamics.flight;
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
/**
* Shift-right-click on a block in the {@code improved_aerodynamics:flappable} tag (Warium's horizontal control
* surfaces) with empty hands makes it a flap, or a normal control surface again. A flap follows the flap lever of the
* control node it is linked to, on top of the pilot's input.
*/
public final class FlapInteraction {
public static final TagKey<Block> FLAPPABLE = TagKey.create(Registries.BLOCK,
new ResourceLocation(ImprovedAerodynamics.MOD_ID, "flappable"));
private FlapInteraction() {
}
@SubscribeEvent
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
Player player = event.getEntity();
if (event.getHand() != InteractionHand.MAIN_HAND || !player.isShiftKeyDown() || player.isPassenger()
|| !player.getMainHandItem().isEmpty() || !player.getOffhandItem().isEmpty()) {
return;
}
Level level = event.getLevel();
BlockPos pos = event.getPos();
BlockState state = level.getBlockState(pos);
if (!state.is(FLAPPABLE)) {
return;
}
// Handled on both sides so the block's own right-click action does not run on either
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide));
if (level.isClientSide) {
return;
}
BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity == null) {
return;
}
CompoundTag tag = blockEntity.getPersistentData();
boolean flap = !tag.getBoolean(WariumCapture.FLAP_TAG);
tag.putBoolean(WariumCapture.FLAP_TAG, flap);
blockEntity.setChanged();
if (flap && !tag.contains("ControlX")) {
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.flap.unlinked"), true);
} else {
player.displayClientMessage(Component.translatable(flap
? "message.improved_aerodynamics.flap.on" : "message.improved_aerodynamics.flap.off"), true);
}
}
}
@@ -1,38 +0,0 @@
package dev.lumareth.improvedAerodynamics.flight;
import dev.lumareth.improvedAerodynamics.Config;
/** Positions of a control node's flap lever. */
public enum FlapSetting {
UP,
TAKEOFF,
LANDING;
/** The setting for a stored number, UP for anything that is not one. */
public static FlapSetting of(int ordinal) {
FlapSetting[] values = values();
return ordinal >= 0 && ordinal < values.length ? values[ordinal] : UP;
}
/** Deflection in degrees the flaps move to at this setting. */
double degrees() {
return switch (this) {
case UP -> 0.0;
case TAKEOFF -> Config.FLAP_TAKEOFF_DEGREES.get();
case LANDING -> Config.FLAP_LANDING_DEGREES.get();
};
}
/** Airspeed in blocks per second above which the air starts to push the flaps back in. Infinite when they are up. */
double maxSpeed() {
return switch (this) {
case UP -> Double.POSITIVE_INFINITY;
case TAKEOFF -> Config.FLAP_TAKEOFF_MAX_SPEED.get();
case LANDING -> Config.FLAP_LANDING_MAX_SPEED.get();
};
}
public String translationKey() {
return "message.improved_aerodynamics.flaps." + name().toLowerCase(java.util.Locale.ROOT);
}
}
@@ -27,12 +27,6 @@ final class ImprovedAero {
private static final double MAX_DEFLECTION = Math.toRadians(20.0); private static final double MAX_DEFLECTION = Math.toRadians(20.0);
// Share of the deflection that acts as angle of attack, gives Warium's control authority at the design speed // Share of the deflection that acts as angle of attack, gives Warium's control authority at the design speed
private static final double FLAP_EFFECTIVENESS = 0.75; private static final double FLAP_EFFECTIVENESS = 0.75;
// How much of the lift a flap adds raises the stall angle, and the parasitic drag coefficient per radian² of flap
// Share of the flap's deflection that acts as angle of attack: 35 degrees of landing flap adds about 1.1 to the
// lift coefficient, like a real plain or slotted flap
private static final double FLAP_ALPHA_FACTOR = 0.4;
private static final double FLAP_STALL_GAIN = 0.7;
private static final double FLAP_DRAG = 0.35;
// Angle of attack of a level cambered wing, where K is calibrated // Angle of attack of a level cambered wing, where K is calibrated
private static final double REFERENCE_ANGLE = Math.toRadians(4.0); private static final double REFERENCE_ANGLE = Math.toRadians(4.0);
private static final double REFERENCE_SPEED = 40.0; private static final double REFERENCE_SPEED = 40.0;
@@ -68,11 +62,10 @@ final class ImprovedAero {
* @param w ship angular velocity, ship axes * @param w ship angular velocity, ship axes
* @param r block center relative to the center of mass, ship axes * @param r block center relative to the center of mass, ship axes
* @param input control input of the surface, -1 to 1 * @param input control input of the surface, -1 to 1
* @param flap flap deflection in radians, 0 for surfaces that are not flaps
* @param stalled whether the surface was stalled in the previous substep * @param stalled whether the surface was stalled in the previous substep
* @return whether the surface is stalled now * @return whether the surface is stalled now
*/ */
static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double flap, double weight, static boolean evaluate(AeroSurface s, double[] v, double[] w, double[] r, double input, double weight,
Tuning tuning, boolean stalled, Wrench out) { Tuning tuning, boolean stalled, Wrench out) {
// Airflow at the block: ship velocity plus w x r // Airflow at the block: ship velocity plus w x r
double vx = v[0] + w[1] * r[2] - w[2] * r[1]; double vx = v[0] + w[1] * r[2] - w[2] * r[1];
@@ -121,10 +114,6 @@ final class ImprovedAero {
// Same sign convention as Warium: positive input pushes along n while moving against the facing // Same sign convention as Warium: positive input pushes along n while moving against the facing
alpha += FLAP_EFFECTIVENESS * MAX_DEFLECTION * input * (u <= 0.0 ? 1.0 : -1.0); alpha += FLAP_EFFECTIVENESS * MAX_DEFLECTION * input * (u <= 0.0 ? 1.0 : -1.0);
} }
// A flap bends the airfoil's camber whichever way the air comes, so it always adds lift, and it moves the stall
// up with it: a flap raises the highest lift a wing can make
double flapShift = FLAP_ALPHA_FACTOR * flap;
alpha += flapShift;
alpha = Math.max(-Math.PI / 2.0, Math.min(Math.PI / 2.0, alpha)); alpha = Math.max(-Math.PI / 2.0, Math.min(Math.PI / 2.0, alpha));
// Low aspect ratio wings keep their flow attached longer thanks to leading-edge vortices. Control surfaces // Low aspect ratio wings keep their flow attached longer thanks to leading-edge vortices. Control surfaces
@@ -135,9 +124,6 @@ final class ImprovedAero {
: s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle; : s.wingGroup() && s.type().controllable ? SurfaceType.WING.stallAngle : s.type().stallAngle;
double critical = Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale(); double critical = Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
double absAlpha = Math.abs(alpha); double absAlpha = Math.abs(alpha);
if (alpha >= 0.0) {
critical += FLAP_STALL_GAIN * flapShift;
}
// Flips only where both blends agree, so the hysteresis never makes the force jump // Flips only where both blends agree, so the hysteresis never makes the force jump
if (!stalled && absAlpha > critical + STALL_BLEND) { if (!stalled && absAlpha > critical + STALL_BLEND) {
stalled = true; stalled = true;
@@ -170,7 +156,7 @@ final class ImprovedAero {
double dy = -(vn * ny) / planeSpeed; double dy = -(vn * ny) / planeSpeed;
double dz = -(u * cz + vn * nz) / planeSpeed; double dz = -(u * cz + vn * nz) / planeSpeed;
double liftForce = q * lift * tuning.liftScale(); double liftForce = q * lift * tuning.liftScale();
double dragForce = q * (drag + FLAP_DRAG * flap * flap) / m; double dragForce = q * drag / m;
out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce); out.addForce(r, lx * liftForce + dx * dragForce, ly * liftForce + dy * dragForce, lz * liftForce + dz * dragForce);
return stalled; return stalled;
} }
@@ -18,7 +18,7 @@ import java.util.Set;
*/ */
public final class MixinPlugin implements IMixinConfigPlugin { public final class MixinPlugin implements IMixinConfigPlugin {
private static final Logger LOGGER = LoggerFactory.getLogger("improved_aerodynamics"); private static final Logger LOGGER = LoggerFactory.getLogger("improved_aerodynamics");
private static final String WARIUM_VERSION = "1.1.7"; private static final String WARIUM_VERSION = "1.1.6";
private static final String FUTURISM_VERSION = "0.0.4"; private static final String FUTURISM_VERSION = "0.0.4";
private boolean warium; private boolean warium;
@@ -5,7 +5,6 @@ package dev.lumareth.improvedAerodynamics.flight;
* trim state. * trim state.
* *
* @param storedTrim trim saved in the node, used when the physics thread first sees the node * @param storedTrim trim saved in the node, used when the physics thread first sees the node
* @param flapSetting the node's flap lever, see {@link FlapSetting#ordinal()}
*/ */
public record NodeData(long key, double pitch, double yaw, double roll, double throttle, boolean autoTrim, double storedTrim, int flapSetting) { public record NodeData(long key, double pitch, double yaw, double roll, double throttle, boolean autoTrim, double storedTrim) {
} }
@@ -1,7 +1,7 @@
package dev.lumareth.improvedAerodynamics.flight; package dev.lumareth.improvedAerodynamics.flight;
/** /**
* Warium VS 1.1.7's aerodynamic forces, term for term as its block procedures compute them, but evaluated from the * Warium VS 1.1.6's aerodynamic forces, term for term as its block procedures compute them, but evaluated from the
* ship's current motion. Like in Warium every block uses the velocity of the ship's center of mass. The procedures' * ship's current motion. Like in Warium every block uses the velocity of the ship's center of mass. The procedures'
* conditions (air around the block, minimum speed, linked control node) were already checked on the server thread. * conditions (air around the block, minimum speed, linked control node) were already checked on the server thread.
* Quirks are kept on purpose so craft fly exactly as before: vertical wings have no drag (Warium never sets the * Quirks are kept on purpose so craft fly exactly as before: vertical wings have no drag (Warium never sets the
@@ -31,8 +31,7 @@ final class WariumAero {
switch (s.type()) { switch (s.type()) {
case WING -> { case WING -> {
out.addForce(r, 0.0, v[1] * Math.abs(v[1]) * -45.0 * p / m, 0.0); out.addForce(r, 0.0, v[1] * Math.abs(v[1]) * -45.0 * p / m, 0.0);
// Lift follows the horizontal speed whichever way the wing faces out.addForce(r, 0.0, Math.abs(fx * v[0] + fz * v[2]) * 160.0 * p, 0.0);
out.addForce(r, 0.0, Math.hypot(v[0], v[2]) * 160.0 * p, 0.0);
double damping = speed * 600.0 * p / m; double damping = speed * 600.0 * p / m;
out.addTorque(-w[0] * damping, -0.1 * w[1] * damping, -w[2] * damping); out.addTorque(-w[0] * damping, -0.1 * w[1] * damping, -w[2] * damping);
double drag = -speed * p / m; double drag = -speed * p / m;
@@ -31,9 +31,6 @@ import org.valkyrienskies.mod.common.VSGameUtilsKt;
public final class WariumCapture { public final class WariumCapture {
public static final String AUTO_TRIM_TAG = "improved_aerodynamics_autotrim"; public static final String AUTO_TRIM_TAG = "improved_aerodynamics_autotrim";
public static final String TRIM_TAG = "improved_aerodynamics_trim"; public static final String TRIM_TAG = "improved_aerodynamics_trim";
/** Block entity flag of a surface that is a flap, and the flap lever of a control node. */
public static final String FLAP_TAG = "improved_aerodynamics_flap";
public static final String FLAP_SETTING_TAG = "improved_aerodynamics_flap_setting";
private static final int TRIM_SAVE_INTERVAL = 20; private static final int TRIM_SAVE_INTERVAL = 20;
// The procedure currently running, procedures run one at a time on the server thread // The procedure currently running, procedures run one at a time on the server thread
@@ -80,12 +77,10 @@ public final class WariumCapture {
String key = "Null"; String key = "Null";
String key2 = "Null"; String key2 = "Null";
double brakeInput = 0.0; double brakeInput = 0.0;
boolean flap = false;
if (type.controllable || type == SurfaceType.AIR_BRAKE) { if (type.controllable || type == SurfaceType.AIR_BRAKE) {
BlockEntity blockEntity = level.getBlockEntity(pos); BlockEntity blockEntity = level.getBlockEntity(pos);
if (blockEntity != null) { if (blockEntity != null) {
CompoundTag tag = blockEntity.getPersistentData(); CompoundTag tag = blockEntity.getPersistentData();
flap = type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(FLAP_TAG);
key = tag.getString("Key"); key = tag.getString("Key");
key2 = type == SurfaceType.AIR_BRAKE ? "Null" : tag.getString("Key2"); key2 = type == SurfaceType.AIR_BRAKE ? "Null" : tag.getString("Key2");
BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ")); BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ"));
@@ -102,7 +97,7 @@ public final class WariumCapture {
return new AeroSurface(pos.asLong(), type, slab, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, return new AeroSurface(pos.asLong(), type, slab, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5,
facing.getStepX(), facing.getStepZ(), pressure, fluidFactor(level), speedMultiplier, facing.getStepX(), facing.getStepZ(), pressure, fluidFactor(level), speedMultiplier,
node, ControlAxis.of(key), ControlAxis.multiplier(key), ControlAxis.of(key2), ControlAxis.multiplier(key2), node, ControlAxis.of(key), ControlAxis.multiplier(key), ControlAxis.of(key2), ControlAxis.multiplier(key2),
brakeInput, 0.0, false, flap); brakeInput, 0.0, false);
} }
/** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */ /** Registers a node's inputs once per tick, and saves its trim to it now and then so it survives reloading. */
@@ -124,8 +119,7 @@ public final class WariumCapture {
} }
} }
aero.addNode(new NodeData(nodeKey, tag.getDouble("Pitch"), tag.getDouble("Yaw"), tag.getDouble("Roll"), aero.addNode(new NodeData(nodeKey, tag.getDouble("Pitch"), tag.getDouble("Yaw"), tag.getDouble("Roll"),
tag.getDouble("Throttle"), autoTrim, storedTrim, tag.getDouble("Throttle"), autoTrim, storedTrim));
FlapSetting.of(tag.getInt(FLAP_SETTING_TAG)).ordinal()));
} }
/** Raw node value for a key like "Pitch+", as Warium's air brakes read it. */ /** Raw node value for a key like "Pitch+", as Warium's air brakes read it. */
@@ -1,33 +0,0 @@
package dev.lumareth.improvedAerodynamics.flight.mixin.warium;
import dev.lumareth.improvedAerodynamics.exhaust.ImprovedExhaust;
import net.mcreator.valkyrienwarium.procedures.BypassedCompressorScriptProcedure;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** The improved exhaust style for the medium bypassed compressor, whose jet effect is an exhaust too: no effect entity, the flame of this mod, see {@link ImprovedExhaust}. */
@Mixin(value = BypassedCompressorScriptProcedure.class, remap = false)
public abstract class BypassedCompressorScriptProcedureMixin {
@Inject(method = "execute", at = @At("HEAD"))
private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
ImprovedExhaust.begin();
}
@Redirect(method = "execute", at = @At(value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerLevel;m_7967_(Lnet/minecraft/world/entity/Entity;)Z"))
private static boolean improvedAerodynamics$addEffect(ServerLevel level, Entity entity) {
return ImprovedExhaust.addEffectEntity(level, entity);
}
@Inject(method = "execute", at = @At("TAIL"))
private static void improvedAerodynamics$plume(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
ImprovedExhaust.plume(world, x, y, z, blockstate);
}
}
@@ -1,33 +0,0 @@
package dev.lumareth.improvedAerodynamics.flight.mixin.warium;
import dev.lumareth.improvedAerodynamics.exhaust.ImprovedExhaust;
import net.mcreator.valkyrienwarium.procedures.JetExhaustOnTickUpdateProcedure;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** The improved exhaust style: no effect entities, a bigger particle plume, see {@link ImprovedExhaust}. */
@Mixin(value = JetExhaustOnTickUpdateProcedure.class, remap = false)
public abstract class JetExhaustOnTickUpdateProcedureMixin {
@Inject(method = "execute", at = @At("HEAD"))
private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
ImprovedExhaust.begin();
}
@Redirect(method = "execute", at = @At(value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerLevel;m_7967_(Lnet/minecraft/world/entity/Entity;)Z"))
private static boolean improvedAerodynamics$addEffect(ServerLevel level, Entity entity) {
return ImprovedExhaust.addEffectEntity(level, entity);
}
@Inject(method = "execute", at = @At("TAIL"))
private static void improvedAerodynamics$plume(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
ImprovedExhaust.plume(world, x, y, z, blockstate);
}
}
@@ -1,33 +0,0 @@
package dev.lumareth.improvedAerodynamics.flight.mixin.warium;
import dev.lumareth.improvedAerodynamics.exhaust.ImprovedExhaust;
import net.mcreator.valkyrienwarium.procedures.VTOLExhaustTickProcedure;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** The improved exhaust style for the VTOL exhaust: no effect entity, the flame of this mod, see {@link ImprovedExhaust}. */
@Mixin(value = VTOLExhaustTickProcedure.class, remap = false)
public abstract class VTOLExhaustTickProcedureMixin {
@Inject(method = "execute", at = @At("HEAD"))
private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
ImprovedExhaust.begin();
}
@Redirect(method = "execute", at = @At(value = "INVOKE",
target = "Lnet/minecraft/server/level/ServerLevel;m_7967_(Lnet/minecraft/world/entity/Entity;)Z"))
private static boolean improvedAerodynamics$addEffect(ServerLevel level, Entity entity) {
return ImprovedExhaust.addEffectEntity(level, entity);
}
@Inject(method = "execute", at = @At("TAIL"))
private static void improvedAerodynamics$plume(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
ImprovedExhaust.plume(world, x, y, z, blockstate);
}
}
@@ -1,45 +0,0 @@
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);
}
}
}
@@ -1,32 +0,0 @@
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);
}
}
@@ -1,39 +0,0 @@
package dev.lumareth.improvedAerodynamics.network;
import dev.lumareth.improvedAerodynamics.exhaust.client.ExhaustFlames;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.fml.DistExecutor;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
/**
* One jet exhaust that burns right now, sent to the players near it every few ticks: where the flame starts and which
* way it points, in the coordinates of the block, which are ship coordinates on a ship. The offset is relative to the
* block because ship coordinates are far out, where floats are too coarse.
*
* @param intensity 0 to 100
*/
public record ExhaustPacket(BlockPos pos, Vec3 exitOffset, Vec3 direction, int intensity, boolean afterburner) {
public void encode(FriendlyByteBuf buffer) {
buffer.writeBlockPos(pos);
buffer.writeFloat((float) exitOffset.x).writeFloat((float) exitOffset.y).writeFloat((float) exitOffset.z);
buffer.writeFloat((float) direction.x).writeFloat((float) direction.y).writeFloat((float) direction.z);
buffer.writeByte(intensity);
buffer.writeBoolean(afterburner);
}
public static ExhaustPacket decode(FriendlyByteBuf buffer) {
BlockPos pos = buffer.readBlockPos();
Vec3 exit = new Vec3(buffer.readFloat(), buffer.readFloat(), buffer.readFloat());
Vec3 direction = new Vec3(buffer.readFloat(), buffer.readFloat(), buffer.readFloat());
return new ExhaustPacket(pos, exit, direction, buffer.readByte(), buffer.readBoolean());
}
public void handle(Supplier<NetworkEvent.Context> context) {
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> ExhaustFlames.accept(this));
}
}
@@ -1,33 +0,0 @@
package dev.lumareth.improvedAerodynamics.network;
import dev.lumareth.improvedAerodynamics.flight.FlapControl;
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 when a pilot moves the flap lever: more flaps (true) or fewer. */
public final class FlapPacket {
private final boolean extend;
public FlapPacket(boolean extend) {
this.extend = extend;
}
public void encode(FriendlyByteBuf buffer) {
buffer.writeBoolean(extend);
}
public static FlapPacket decode(FriendlyByteBuf buffer) {
return new FlapPacket(buffer.readBoolean());
}
public void handle(Supplier<NetworkEvent.Context> context) {
ServerPlayer player = context.get().getSender();
if (player != null && ModList.get().isLoaded("valkyrien_warium")) {
FlapControl.step(player, extend ? 1 : -1);
}
}
}
@@ -23,32 +23,5 @@ public final class Network {
.decoder(ToggleAutoTrimPacket::decode) .decoder(ToggleAutoTrimPacket::decode)
.consumerMainThread(ToggleAutoTrimPacket::handle) .consumerMainThread(ToggleAutoTrimPacket::handle)
.add(); .add();
CHANNEL.messageBuilder(ToggleAfterburnerPacket.class, 1, NetworkDirection.PLAY_TO_SERVER)
.encoder(ToggleAfterburnerPacket::encode)
.decoder(ToggleAfterburnerPacket::decode)
.consumerMainThread(ToggleAfterburnerPacket::handle)
.add();
CHANNEL.messageBuilder(ExhaustPacket.class, 2, NetworkDirection.PLAY_TO_CLIENT)
.encoder(ExhaustPacket::encode)
.decoder(ExhaustPacket::decode)
.consumerMainThread(ExhaustPacket::handle)
.add();
// High id so it cannot collide with packets added on other branches
CHANNEL.messageBuilder(AimPacket.class, 50, NetworkDirection.PLAY_TO_SERVER)
.encoder(AimPacket::encode)
.decoder(AimPacket::decode)
.consumerMainThread(AimPacket::handle)
.add();
CHANNEL.messageBuilder(ControlModePacket.class, 51, NetworkDirection.PLAY_TO_CLIENT)
.encoder(ControlModePacket::encode)
.decoder(ControlModePacket::decode)
.consumerMainThread(ControlModePacket::handle)
.add();
// Higher id than the neighbours so it does not collide with packets added on other branches
CHANNEL.messageBuilder(FlapPacket.class, 10, NetworkDirection.PLAY_TO_SERVER)
.encoder(FlapPacket::encode)
.decoder(FlapPacket::decode)
.consumerMainThread(FlapPacket::handle)
.add();
} }
} }
@@ -1,26 +0,0 @@
package dev.lumareth.improvedAerodynamics.network;
import dev.lumareth.improvedAerodynamics.exhaust.AfterburnerToggle;
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 when a player presses the afterburner key, toggles the afterburner on the control node of the seat they sit in. */
public final class ToggleAfterburnerPacket {
public void encode(FriendlyByteBuf buffer) {
}
public static ToggleAfterburnerPacket decode(FriendlyByteBuf buffer) {
return new ToggleAfterburnerPacket();
}
public void handle(Supplier<NetworkEvent.Context> context) {
ServerPlayer player = context.get().getSender();
if (player != null && ModList.get().isLoaded("valkyrien_warium")) {
AfterburnerToggle.toggle(player);
}
}
}
@@ -1,95 +0,0 @@
package dev.lumareth.improvedAerodynamics.terrain;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.saveddata.SavedData;
import java.util.HashMap;
import java.util.Map;
import java.util.OptionalInt;
/**
* Per dimension, the height above which terrain is known to be air, measured from one real chunk with
* {@code /aero seedgen}. The air placeholder hands VS only air above it, which beats the noise based estimate in
* worlds where that does not work, like flat or void worlds. Saved with the world.
*/
public final class AirCeiling extends SavedData {
private static final String NAME = "improved_aerodynamics_air_ceilings";
/** The blocks that are read, clamped to the dimension's build height. */
public static final int SCAN_MIN_Y = -64;
public static final int SCAN_MAX_Y = 100;
/** Outcome of reading a chunk. */
public record Analysis(boolean complete, int highestSolid, int airHeight) {
}
private final Map<ResourceLocation, Integer> ceilings = new HashMap<>();
public static AirCeiling get(ServerLevel level) {
return level.getServer().overworld().getDataStorage().computeIfAbsent(AirCeiling::load, AirCeiling::new, NAME);
}
private static AirCeiling load(CompoundTag tag) {
AirCeiling ceiling = new AirCeiling();
CompoundTag saved = tag.getCompound("ceilings");
for (String key : saved.getAllKeys()) {
ceiling.ceilings.put(new ResourceLocation(key), saved.getInt(key));
}
return ceiling;
}
@Override
public CompoundTag save(CompoundTag tag) {
CompoundTag saved = new CompoundTag();
ceilings.forEach((dimension, height) -> saved.putInt(dimension.toString(), height));
tag.put("ceilings", saved);
return tag;
}
/** Lowest Y at or above which this dimension has only air, if it was measured. */
public OptionalInt ceiling(ResourceKey<Level> dimension) {
Integer height = ceilings.get(dimension.location());
return height != null ? OptionalInt.of(height) : OptionalInt.empty();
}
public void set(ResourceKey<Level> dimension, int height) {
ceilings.put(dimension.location(), height);
setDirty();
}
public boolean clear(ResourceKey<Level> dimension) {
boolean removed = ceilings.remove(dimension.location()) != null;
if (removed) {
setDirty();
}
return removed;
}
/**
* Reads the chunk's blocks (generating the chunk if it does not exist yet) from the top of the scan range down
* and finds the first layer with anything but air in it. Not complete if that layer is the top of the range,
* since then the air may start higher up than what was read.
*/
public static Analysis analyze(ServerLevel level, ChunkPos pos) {
ChunkAccess chunk = level.getChunk(pos.x, pos.z);
int bottom = Math.max(SCAN_MIN_Y, level.getMinBuildHeight());
int top = Math.min(SCAN_MAX_Y, level.getMaxBuildHeight() - 1);
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
for (int y = top; y >= bottom; y--) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (!chunk.getBlockState(cursor.set(pos.getMinBlockX() + x, y, pos.getMinBlockZ() + z)).isAir()) {
return new Analysis(y < top, y, y + 1);
}
}
}
}
return new Analysis(true, bottom - 1, bottom);
}
}
@@ -22,13 +22,11 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.OptionalInt;
/** /**
* Optional: hands VS empty sections for not yet loaded chunks on a ship's path, so physics sees air there instead of * Optional: hands VS empty sections for not yet loaded chunks on a ship's path, so physics sees air there instead of
* its solid "unloaded" fallback. Only sections fully above the generator's estimated surface (plus a margin) are * its solid "unloaded" fallback. Only sections fully above the generator's estimated surface (plus a margin) are
* replaced, everything lower stays solid so a ship can never end up inside terrain that has not loaded yet. * replaced, everything lower stays solid so a ship can never end up inside terrain that has not loaded yet.
* If a height was measured with /aero seedgen, every section from there up is replaced instead.
* Once the real chunk loads, VS overwrites every section of it with the actual terrain. * Once the real chunk loads, VS overwrites every section of it with the actual terrain.
* <p> * <p>
* This uses VS internals (its terrain update API and the mixin field listing its loaded chunks), so it switches * This uses VS internals (its terrain update API and the mixin field listing its loaded chunks), so it switches
@@ -78,8 +76,6 @@ final class AirPlaceholder {
if (enabled) { if (enabled) {
int budget = Config.MAX_NEW_CHUNKS_PER_TICK.get(); int budget = Config.MAX_NEW_CHUNKS_PER_TICK.get();
int surfaceMargin = Config.PLACEHOLDER_SURFACE_MARGIN.get(); int surfaceMargin = Config.PLACEHOLDER_SURFACE_MARGIN.get();
// Measured with /aero seedgen, replaces the estimate (and its margin) for this dimension
OptionalInt airHeight = AirCeiling.get(level).ceiling(level.dimension());
for (long chunk : corridor) { for (long chunk : corridor) {
Placeholder placeholder = placeholders.get(chunk); Placeholder placeholder = placeholders.get(chunk);
if (placeholder != null) { if (placeholder != null) {
@@ -94,8 +90,7 @@ final class AirPlaceholder {
if (budget-- <= 0) { if (budget-- <= 0) {
break; break;
} }
int minSectionY = airHeight.isPresent() ? Math.floorDiv(airHeight.getAsInt() + 15, 16) int minSectionY = Math.floorDiv(estimateSurface(level, pos) + surfaceMargin, 16) + 1;
: Math.floorDiv(estimateSurface(level, pos) + surfaceMargin, 16) + 1;
int maxSectionY = level.getMaxSection() - 1; int maxSectionY = level.getMaxSection() - 1;
if (minSectionY > maxSectionY) { if (minSectionY > maxSectionY) {
continue; continue;
-8
View File
@@ -45,11 +45,3 @@ mandatory = false
versionRange = "[0.0.4,)" versionRange = "[0.0.4,)"
ordering = "AFTER" ordering = "AFTER"
side = "BOTH" side = "BOTH"
# Optional, animated block models
[[dependencies."${mod_id}"]]
modId = "azurelib"
mandatory = false
versionRange = "[3.2.7,)"
ordering = "AFTER"
side = "BOTH"
@@ -1,8 +0,0 @@
{
"format_version": "1.8.0",
"animations": {
"animation.model.nozzle": {
"loop": true
}
}
}
@@ -1,7 +0,0 @@
{
"variants": {
"": {
"model": "improved_aerodynamics:block/vectored_afterburner_nozzle"
}
}
}
@@ -1,113 +0,0 @@
{
"format_version": "1.21.20",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.unknown",
"texture_width": 16,
"texture_height": 16,
"visible_bounds_width": 3,
"visible_bounds_height": 2.5,
"visible_bounds_offset": [0, 0.75, 0]
},
"bones": [
{
"name": "bb_main",
"pivot": [0, 0, 0],
"cubes": [
{
"origin": [-16, 0, 10],
"size": [16, 16, 6],
"uv": {
"north": {"uv": [1.5, 1.5], "uv_size": [4, 4]},
"east": {"uv": [0, 1.5], "uv_size": [1.5, 4]},
"south": {"uv": [7, 1.5], "uv_size": [4, 4]},
"west": {"uv": [5.5, 1.5], "uv_size": [1.5, 4]},
"up": {"uv": [1.5, 0], "uv_size": [4, 1.5]},
"down": {"uv": [5.5, 1.5], "uv_size": [4, -1.5]}
}
}
]
},
{
"name": "hinge",
"pivot": [-8, 8, 10],
"rotation": [0, 0, 32.5]
},
{
"name": "rotation",
"parent": "hinge",
"pivot": [-8, 8, 10]
},
{
"name": "counter",
"parent": "rotation",
"pivot": [-8, 8, 10],
"rotation": [0, 0, -32.5],
"cubes": [
{
"origin": [-15, 1, 0],
"size": [14, 2, 10],
"uv": {
"north": {"uv": [2.5, 8], "uv_size": [3.5, 0.5]},
"east": {"uv": [0, 8], "uv_size": [2.5, 0.5]},
"south": {"uv": [8.5, 8], "uv_size": [3.5, 0.5]},
"west": {"uv": [6, 8], "uv_size": [2.5, 0.5]},
"up": {"uv": [2.5, 5.5], "uv_size": [3.5, 2.5]},
"down": {"uv": [6, 8], "uv_size": [3.5, -2.5]}
}
},
{
"origin": [-15, 13, 0],
"size": [14, 2, 10],
"uv": {
"north": {"uv": [6.5, 15.5], "uv_size": [3.5, 0.5]},
"east": {"uv": [4, 15.5], "uv_size": [2.5, 0.5]},
"south": {"uv": [12.5, 15.5], "uv_size": [3.5, 0.5]},
"west": {"uv": [10, 15.5], "uv_size": [2.5, 0.5]},
"up": {"uv": [6.5, 13], "uv_size": [3.5, 2.5]},
"down": {"uv": [10, 15.5], "uv_size": [3.5, -2.5]}
}
},
{
"origin": [-15, 3, 0],
"size": [2, 10, 10],
"uv": {
"north": {"uv": [2.5, 11], "uv_size": [0.5, 2.5]},
"east": {"uv": [0, 11], "uv_size": [2.5, 2.5]},
"south": {"uv": [5.5, 11], "uv_size": [0.5, 2.5]},
"west": {"uv": [3, 11], "uv_size": [2.5, 2.5]},
"up": {"uv": [2.5, 8.5], "uv_size": [0.5, 2.5]},
"down": {"uv": [3, 11], "uv_size": [0.5, -2.5]}
}
},
{
"origin": [-3, 3, 0],
"size": [2, 10, 10],
"uv": {
"north": {"uv": [3, 11], "uv_size": [-0.5, 2.5]},
"east": {"uv": [5.5, 11], "uv_size": [-2.5, 2.5]},
"south": {"uv": [6, 11], "uv_size": [-0.5, 2.5]},
"west": {"uv": [2.5, 11], "uv_size": [-2.5, 2.5]},
"up": {"uv": [3, 8.5], "uv_size": [-0.5, 2.5]},
"down": {"uv": [3.5, 11], "uv_size": [-0.5, -2.5]}
}
},
{
"origin": [-11, 5, 4],
"size": [6, 6, 6],
"uv": {
"north": {"uv": [7.5, 10], "uv_size": [1.5, 1.5]},
"east": {"uv": [6, 10], "uv_size": [1.5, 1.5]},
"south": {"uv": [10.5, 10], "uv_size": [1.5, 1.5]},
"west": {"uv": [9, 10], "uv_size": [1.5, 1.5]},
"up": {"uv": [7.5, 8.5], "uv_size": [1.5, 1.5]},
"down": {"uv": [9, 10], "uv_size": [1.5, -1.5]}
}
}
]
}
]
}
]
}
@@ -1,8 +1,8 @@
{ {
"key.categories.improved_aerodynamics": "Improved Aerodynamics", "key.categories.improved_aerodynamics": "Improved Aerodynamics",
"key.improved_aerodynamics.toggle_auto_trim": "Automatische Trimmung umschalten", "key.improved_aerodynamics.toggle_auto_trim": "Automatische Trimmung umschalten",
"message.improved_aerodynamics.auto_trim.on": "Flugcomputer an (automatische Trimmung)", "message.improved_aerodynamics.auto_trim.on": "Automatische Trimmung an",
"message.improved_aerodynamics.auto_trim.off": "Flugcomputer aus (automatische Trimmung)", "message.improved_aerodynamics.auto_trim.off": "Automatische Trimmung aus",
"message.improved_aerodynamics.auto_trim.unavailable": "Automatische Trimmung an, sie braucht aber physicsRateAerodynamics in der Serverkonfiguration", "message.improved_aerodynamics.auto_trim.unavailable": "Automatische Trimmung an, sie braucht aber physicsRateAerodynamics in der Serverkonfiguration",
"message.improved_aerodynamics.auto_trim.no_seat": "Setz dich in einen Warium-Steuersitz, um die automatische Trimmung zu nutzen", "message.improved_aerodynamics.auto_trim.no_seat": "Setz dich in einen Warium-Steuersitz, um die automatische Trimmung zu nutzen",
"message.improved_aerodynamics.auto_trim.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden", "message.improved_aerodynamics.auto_trim.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden",
@@ -12,31 +12,5 @@
"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"
"block.improved_aerodynamics.vectored_afterburner_nozzle": "Schubvektor-Nachbrennerdüse",
"itemGroup.improved_aerodynamics": "Improved Aerodynamics",
"message.improved_aerodynamics.nozzle.left": "Düse auf linkes Triebwerk gestellt",
"message.improved_aerodynamics.nozzle.right": "Düse auf rechtes Triebwerk gestellt",
"key.improved_aerodynamics.toggle_afterburner": "Nachbrenner umschalten",
"message.improved_aerodynamics.afterburner.on": "Nachbrenner an",
"message.improved_aerodynamics.afterburner.off": "Nachbrenner aus",
"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_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden",
"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",
"key.improved_aerodynamics.flaps_down": "Klappen runter (eine Stufe)",
"key.improved_aerodynamics.flaps_up": "Klappen rauf (eine Stufe)",
"message.improved_aerodynamics.flaps": "Klappen: %s",
"message.improved_aerodynamics.flaps.up": "eingefahren",
"message.improved_aerodynamics.flaps.takeoff": "Start",
"message.improved_aerodynamics.flaps.landing": "Landung",
"message.improved_aerodynamics.flaps.no_seat": "Setz dich in einen Warium-Steuersitz, um die Klappen zu nutzen",
"message.improved_aerodynamics.flaps.no_node": "Dieser Sitz ist mit keinem Vehicle Control Node verbunden",
"message.improved_aerodynamics.flap.on": "Das Ruder ist jetzt eine Klappe",
"message.improved_aerodynamics.flap.off": "Das Ruder ist wieder ein normales Ruder",
"message.improved_aerodynamics.flap.unlinked": "Das Ruder ist jetzt eine Klappe, aber noch mit keinem Vehicle Control Node verbunden"
} }
@@ -1,8 +1,8 @@
{ {
"key.categories.improved_aerodynamics": "Improved Aerodynamics", "key.categories.improved_aerodynamics": "Improved Aerodynamics",
"key.improved_aerodynamics.toggle_auto_trim": "Toggle auto trim", "key.improved_aerodynamics.toggle_auto_trim": "Toggle auto trim",
"message.improved_aerodynamics.auto_trim.on": "Flight computer on (auto trim)", "message.improved_aerodynamics.auto_trim.on": "Auto trim on",
"message.improved_aerodynamics.auto_trim.off": "Flight computer off (auto trim)", "message.improved_aerodynamics.auto_trim.off": "Auto trim off",
"message.improved_aerodynamics.auto_trim.unavailable": "Auto trim on, but it needs physicsRateAerodynamics enabled in the server config", "message.improved_aerodynamics.auto_trim.unavailable": "Auto trim on, but it needs physicsRateAerodynamics enabled in the server config",
"message.improved_aerodynamics.auto_trim.no_seat": "Sit in a Warium control seat to use auto trim", "message.improved_aerodynamics.auto_trim.no_seat": "Sit in a Warium control seat to use auto trim",
"message.improved_aerodynamics.auto_trim.no_node": "This seat is not linked to a vehicle control node", "message.improved_aerodynamics.auto_trim.no_node": "This seat is not linked to a vehicle control node",
@@ -12,31 +12,5 @@
"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"
"block.improved_aerodynamics.vectored_afterburner_nozzle": "Vectored Afterburner Nozzle",
"itemGroup.improved_aerodynamics": "Improved Aerodynamics",
"message.improved_aerodynamics.nozzle.left": "Nozzle set to left engine",
"message.improved_aerodynamics.nozzle.right": "Nozzle set to right engine",
"key.improved_aerodynamics.toggle_afterburner": "Toggle afterburner",
"message.improved_aerodynamics.afterburner.on": "Afterburner on",
"message.improved_aerodynamics.afterburner.off": "Afterburner off",
"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_node": "This seat is not linked to a vehicle control node",
"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",
"key.improved_aerodynamics.flaps_down": "Flaps down (one step)",
"key.improved_aerodynamics.flaps_up": "Flaps up (one step)",
"message.improved_aerodynamics.flaps": "Flaps: %s",
"message.improved_aerodynamics.flaps.up": "up",
"message.improved_aerodynamics.flaps.takeoff": "takeoff",
"message.improved_aerodynamics.flaps.landing": "landing",
"message.improved_aerodynamics.flaps.no_seat": "Sit in a Warium control seat to use the flaps",
"message.improved_aerodynamics.flaps.no_node": "This seat is not linked to a vehicle control node",
"message.improved_aerodynamics.flap.on": "Control surface is now a flap",
"message.improved_aerodynamics.flap.off": "Control surface is a normal control surface again",
"message.improved_aerodynamics.flap.unlinked": "Control surface is now a flap, but it is not linked to a vehicle control node yet"
} }
@@ -1,5 +0,0 @@
{
"textures": {
"particle": "minecraft:block/iron_block"
}
}
@@ -1,6 +0,0 @@
{
"parent": "minecraft:block/cube_all",
"textures": {
"all": "minecraft:block/iron_block"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1,20 +0,0 @@
{
"type": "minecraft:block",
"pools": [
{
"rolls": 1.0,
"bonus_rolls": 0.0,
"entries": [
{
"type": "minecraft:item",
"name": "improved_aerodynamics:vectored_afterburner_nozzle"
}
],
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
]
}
]
}
@@ -1,221 +0,0 @@
{
"replace": false,
"values": [
{
"id": "valkyrien_warium:control_surface",
"required": false
},
{
"id": "valkyrien_warium:control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:control_surface_dark_gray",
"required": false
},
{
"id": "valkyrien_warium:control_surface_offset_top_dark_gray",
"required": false
},
{
"id": "valkyrien_warium:control_surface_offset_bottom_dark_gray",
"required": false
},
{
"id": "valkyrien_warium:wooden_control_surface",
"required": false
},
{
"id": "valkyrien_warium:wooden_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:wooden_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:red_control_surface",
"required": false
},
{
"id": "valkyrien_warium:red_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:red_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:blue_control_surface",
"required": false
},
{
"id": "valkyrien_warium:blue_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:blue_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:brown_control_surface",
"required": false
},
{
"id": "valkyrien_warium:brown_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:brown_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:green_control_surface",
"required": false
},
{
"id": "valkyrien_warium:green_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:cyan_control_surface",
"required": false
},
{
"id": "valkyrien_warium:cyan_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:cyan_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:orange_control_surface",
"required": false
},
{
"id": "valkyrien_warium:orange_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:orange_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:yellow_control_surface",
"required": false
},
{
"id": "valkyrien_warium:yellow_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:yellow_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:gray_control_surface",
"required": false
},
{
"id": "valkyrien_warium:gray_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:gray_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:light_gray_control_surface",
"required": false
},
{
"id": "valkyrien_warium:light_gray_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:light_gray_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:light_blue_control_surface",
"required": false
},
{
"id": "valkyrien_warium:light_blue_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:light_blue_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:pink_control_surface",
"required": false
},
{
"id": "valkyrien_warium:pink_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:pink_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:magenta_control_surface",
"required": false
},
{
"id": "valkyrien_warium:magenta_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:magenta_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:purple_control_surface",
"required": false
},
{
"id": "valkyrien_warium:purple_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:purple_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:white_control_surface",
"required": false
},
{
"id": "valkyrien_warium:white_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:white_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:black_control_surface",
"required": false
},
{
"id": "valkyrien_warium:black_control_surface_offset_top",
"required": false
},
{
"id": "valkyrien_warium:black_control_surface_offset_bottom",
"required": false
},
{
"id": "valkyrien_warium:green_control_surface_offset_top",
"required": false
}
]
}
@@ -1,7 +0,0 @@
[
{
"block": "improved_aerodynamics:vectored_afterburner_nozzle",
"mass": 500.0,
"priority": 101
}
]
@@ -1,6 +0,0 @@
{
"replace": false,
"values": [
"improved_aerodynamics:vectored_afterburner_nozzle"
]
}
@@ -1,6 +0,0 @@
{
"replace": false,
"values": [
"improved_aerodynamics:vectored_afterburner_nozzle"
]
}
@@ -1,6 +0,0 @@
{
"replace": false,
"values": [
"improved_aerodynamics:vectored_afterburner_nozzle"
]
}
@@ -1,6 +0,0 @@
{
"replace": false,
"values": [
"improved_aerodynamics:vectored_afterburner_nozzle"
]
}
@@ -1,9 +0,0 @@
{
"replace": false,
"values": [
{ "id": "valkyrien_warium:slim_landing_gear_bay", "required": false },
{ "id": "valkyrien_warium:open_slim_landing_gear_bay", "required": false },
{ "id": "warframes:airframe_slim_landing_gear_bay", "required": false },
{ "id": "warframes:airframe_slab_landing_gear_bay", "required": false }
]
}
@@ -7,13 +7,10 @@
"mixins": [ "mixins": [
"futurism.ForceInducedShipsMixin", "futurism.ForceInducedShipsMixin",
"warium.AirBrakeTickProcedureMixin", "warium.AirBrakeTickProcedureMixin",
"warium.BypassedCompressorScriptProcedureMixin",
"warium.ControlSurfaceTickProcedureMixin", "warium.ControlSurfaceTickProcedureMixin",
"warium.ForceInducedShipsMixin", "warium.ForceInducedShipsMixin",
"warium.HorizontalStabilizerScriptProcedureMixin", "warium.HorizontalStabilizerScriptProcedureMixin",
"warium.HorizontalWingScriptProcedureMixin", "warium.HorizontalWingScriptProcedureMixin",
"warium.JetExhaustOnTickUpdateProcedureMixin",
"warium.VTOLExhaustTickProcedureMixin",
"warium.VerticalAirBrakeTickProcedureMixin", "warium.VerticalAirBrakeTickProcedureMixin",
"warium.VerticalControlSurfaceTickProcedureMixin", "warium.VerticalControlSurfaceTickProcedureMixin",
"warium.VerticalWingScriptProcedureMixin" "warium.VerticalWingScriptProcedureMixin"