Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
433311a1c9 | ||
|
|
0c8af898ae | ||
|
|
3c7889796b | ||
|
|
44a4390f69 | ||
|
|
6a8a7ead04 | ||
|
|
9e7aae5773 |
No files matched your search
+2
-12
@@ -58,13 +58,6 @@ repositories {
|
||||
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/
|
||||
flatDir {
|
||||
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 '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)
|
||||
compileOnly fg.deobf("blank:WariumVS:1.1.7")
|
||||
// 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.6")
|
||||
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
@@ -12,8 +12,7 @@ mapping_version=2023.09.03-1.20.1
|
||||
mod_id=improved_aerodynamics
|
||||
mod_name=VS: Improved Aerodynamics
|
||||
mod_license=MIT
|
||||
mod_version=0.4.0
|
||||
azurelib_version=3.2.7
|
||||
mod_version=0.3.0
|
||||
mod_group_id=dev.lumareth.improvedaerodynamics
|
||||
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.
|
||||
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")
|
||||
.defineInRange("vortexLiftStrength", 1.0D, 0.0D, 3.0D);
|
||||
|
||||
public static final ForgeConfigSpec.DoubleValue FBW_AUTHORITY = BUILDER
|
||||
.comment("Flight computer (auto trim): largest total pitch input, as a fraction of full deflection, the computer may use.",
|
||||
"A craft with strongly relaxed stability needs close to 1")
|
||||
.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_AUTHORITY = BUILDER
|
||||
.comment("Auto trim: largest pitch input (fraction of full deflection) the trim may hold")
|
||||
.defineInRange("trimAuthority", 0.5D, 0.0D, 1.0D);
|
||||
|
||||
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);
|
||||
|
||||
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 {
|
||||
BUILDER.pop();
|
||||
}
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,11 @@ package dev.lumareth.improvedAerodynamics;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import dev.lumareth.improvedAerodynamics.exhaust.NozzleCommands;
|
||||
import dev.lumareth.improvedAerodynamics.flight.ControlMode;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
@@ -27,11 +24,7 @@ import java.util.List;
|
||||
* All of them are read live, so changes apply immediately.
|
||||
*/
|
||||
public final class ConfigCommands {
|
||||
private record Option<T>(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type,
|
||||
List<String> choices) {
|
||||
Option(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type) {
|
||||
this(name, value, argument, type, List.of());
|
||||
}
|
||||
private record Option<T>(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type) {
|
||||
}
|
||||
|
||||
private static final List<Option<?>> OPTIONS = List.of(
|
||||
@@ -50,24 +43,9 @@ public final class ConfigCommands {
|
||||
new Option<>("vortex_lift", Config.VORTEX_LIFT, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("vortex_stall_bonus", Config.VORTEX_STALL_BONUS, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("vortex_lift_strength", Config.VORTEX_LIFT_STRENGTH, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_authority", Config.FBW_AUTHORITY, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_rate_gain", Config.FBW_RATE_GAIN, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_integral_gain", Config.FBW_INTEGRAL_GAIN, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_stick_rate_degrees", Config.FBW_STICK_RATE_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flight_computer_actuator_rate", Config.FBW_ACTUATOR_RATE, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("trim_authority", Config.TRIM_AUTHORITY, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("trim_min_speed", Config.TRIM_MIN_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("air_placeholder", Config.ENABLE_AIR_PLACEHOLDER, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("nozzle_max_deflection", Config.NOZZLE_MAX_DEFLECTION, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("nozzle_slew_rate", Config.NOZZLE_SLEW_RATE, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("afterburner_min_throttle", Config.AFTERBURNER_MIN_THROTTLE, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("exhaust_style", Config.EXHAUST_STYLE, StringArgumentType.word(), String.class, List.of("wariumvs", "improved")),
|
||||
new Option<>("nozzle_yaw_mix", Config.NOZZLE_YAW_MIX, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("nozzle_light", Config.NOZZLE_LIGHT, BoolArgumentType.bool(), Boolean.class),
|
||||
new Option<>("flap_takeoff_degrees", Config.FLAP_TAKEOFF_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flap_landing_degrees", Config.FLAP_LANDING_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flap_slew_degrees", Config.FLAP_SLEW_DEGREES, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flap_takeoff_max_speed", Config.FLAP_TAKEOFF_MAX_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("flap_landing_max_speed", Config.FLAP_LANDING_MAX_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
||||
new Option<>("lookahead_seconds", Config.LOOKAHEAD_SECONDS, DoubleArgumentType.doubleArg(), Double.class));
|
||||
|
||||
private ConfigCommands() {
|
||||
@@ -90,8 +68,6 @@ public final class ConfigCommands {
|
||||
for (Option<?> option : OPTIONS) {
|
||||
root.then(optionNode(option).requires(ConfigCommands::mayUse));
|
||||
}
|
||||
root.then(SeedgenCommands.node().requires(ConfigCommands::mayUse));
|
||||
root.then(NozzleCommands.node().requires(ConfigCommands::mayUse));
|
||||
root.then(controlsNode());
|
||||
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
|
||||
event.getDispatcher().register(Commands.literal("aero")
|
||||
@@ -128,12 +104,10 @@ public final class ConfigCommands {
|
||||
private static <T> LiteralArgumentBuilder<CommandSourceStack> optionNode(Option<T> option) {
|
||||
return Commands.literal(option.name())
|
||||
.executes(context -> {
|
||||
context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get()
|
||||
+ (option.choices().isEmpty() ? "" : " (" + String.join(" or ", option.choices()) + ")")), false);
|
||||
context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get()), false);
|
||||
return 1;
|
||||
})
|
||||
.then(Commands.argument("value", option.argument())
|
||||
.suggests((context, builder) -> SharedSuggestionProvider.suggest(option.choices(), builder))
|
||||
.executes(context -> set(context.getSource(), option, context.getArgument("value", option.type()))));
|
||||
}
|
||||
|
||||
@@ -143,8 +117,7 @@ public final class ConfigCommands {
|
||||
ForgeConfigSpec.Range<?> range = spec.getRange();
|
||||
source.sendFailure(Component.literal(range != null
|
||||
? option.name() + " must be between " + range.getMin() + " and " + range.getMax()
|
||||
: option.choices().isEmpty() ? "Invalid value for " + option.name()
|
||||
: option.name() + " must be " + String.join(" or ", option.choices())));
|
||||
: "Invalid value for " + option.name()));
|
||||
return 0;
|
||||
}
|
||||
option.value().set(newValue);
|
||||
|
||||
@@ -3,8 +3,6 @@ package dev.lumareth.improvedAerodynamics;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.lumareth.improvedAerodynamics.client.aim.CursorAimClientConfig;
|
||||
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.network.Network;
|
||||
@@ -13,7 +11,6 @@ import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.fml.ModLoadingContext;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
@Mod(ImprovedAerodynamics.MOD_ID)
|
||||
@@ -26,16 +23,11 @@ public class ImprovedAerodynamics {
|
||||
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
||||
MinecraftForge.EVENT_BUS.register(PhysicsBridge.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.CLIENT, Config.CLIENT_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.CLIENT, PipperConfig.SPEC);
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, CursorAimConfig.SPEC, "improved_aerodynamics-cursor-aim.toml");
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, CursorAimClientConfig.SPEC, "improved_aerodynamics-cursor-aim-client.toml");
|
||||
NozzleContent.register(FMLJavaModLoadingContext.get().getModEventBus());
|
||||
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 dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
||||
import dev.lumareth.improvedAerodynamics.network.FlapPacket;
|
||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||
import dev.lumareth.improvedAerodynamics.network.ToggleAfterburnerPacket;
|
||||
import dev.lumareth.improvedAerodynamics.network.ToggleAutoTrimPacket;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
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",
|
||||
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() {
|
||||
}
|
||||
|
||||
@@ -40,9 +29,6 @@ public final class KeyBindings {
|
||||
@SubscribeEvent
|
||||
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-113
@@ -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;
|
||||
}
|
||||
}
|
||||
-24
@@ -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
|
||||
* motion in each substep until the next tick arrives.
|
||||
* <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.
|
||||
*/
|
||||
public final class AeroState implements PhysicsBridge.Publishable {
|
||||
@@ -55,10 +55,6 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double roll;
|
||||
double trimIntegral;
|
||||
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
|
||||
final Vector3d pitchResponse = new Vector3d();
|
||||
final Vector3d pitchResponseBuilding = new Vector3d();
|
||||
@@ -83,10 +79,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
yaw = data.yaw();
|
||||
roll = data.roll();
|
||||
trimIntegral = data.autoTrim() ? data.storedTrim() : 0.0;
|
||||
trimOutput = data.autoTrim() ? 0.0 : trimIntegral;
|
||||
trimOutput = trimIntegral;
|
||||
savedTrim = trimIntegral;
|
||||
computerOutput = pitch + trimOutput;
|
||||
flapAngle = Math.toRadians(FlapSetting.of(data.flapSetting()).degrees());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,10 +238,9 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : 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 flap = s.flap() && state != null ? state.flapAngle : 0.0;
|
||||
|
||||
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) {
|
||||
stalled[i] = nowStalled;
|
||||
wingStalled |= nowStalled && s.type() == SurfaceType.WING;
|
||||
@@ -267,7 +260,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
double delta = 0.05;
|
||||
probe.clear();
|
||||
if (improved) {
|
||||
ImprovedAero.evaluate(s, v, w, r, input, flap, weight, tuning, stalled[i], probe);
|
||||
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
|
||||
} else {
|
||||
WariumAero.evaluate(s, v, w, r, input, weight, probe);
|
||||
}
|
||||
@@ -275,7 +268,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
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);
|
||||
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
|
||||
} else {
|
||||
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
||||
}
|
||||
@@ -320,8 +313,6 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
private void updateNodes(Frame frame, Vector3dc worldVelocity, double altitude, Quaterniondc rotation, Matrix3dc inertia,
|
||||
double[] w, double dt, boolean improved) {
|
||||
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()) {
|
||||
NodeState state = nodeStates.get(node.key());
|
||||
if (state == null) {
|
||||
@@ -332,25 +323,10 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
||||
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
||||
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
||||
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);
|
||||
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, 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) {
|
||||
if (Math.abs(target - value) <= maxStep) {
|
||||
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 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 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,
|
||||
double pressure, double fluidFactor, double speedMultiplier,
|
||||
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;
|
||||
|
||||
AeroSurface withWingShape(double aspectRatio, boolean wingGroup) {
|
||||
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. */
|
||||
|
||||
@@ -7,28 +7,19 @@ import org.joml.Vector3d;
|
||||
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
|
||||
* system: the pilot's pitch input is a pitch rate command, not a surface deflection, and the computer moves the
|
||||
* surfaces to make the craft follow it, whatever the airframe does on its own.
|
||||
* Auto trim for the pitch control surfaces of one control node: while the pilot gives no pitch input it holds the
|
||||
* altitude the craft had when the pilot let go, cancelling whatever pitching moment the craft has (off-center weight,
|
||||
* thrust line, speed changes).
|
||||
* <p>
|
||||
* Every physics substep it measures the pitching torque the craft makes with the pitch surfaces taken out
|
||||
* ({@code external}: weight offset, thrust line, and above all the airframe's own stability or instability), and the
|
||||
* torque one unit of pitch input makes ({@code response}). It then picks the input that makes the craft's total
|
||||
* pitch torque equal the angular acceleration it wants, {@code rateGain * (rate command - pitch rate)} plus a small
|
||||
* 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.
|
||||
* Three nested loops: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than
|
||||
* {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate, and a PI controller moves the trim to reach
|
||||
* that rate. Bounding the attitude matters: in Warium's physics the flight path follows the nose only slowly, so
|
||||
* steering the flight path directly with pitch rate keeps rotating the nose until the craft dives.
|
||||
* <p>
|
||||
* Without pilot input the rate command comes from three nested loops that hold the altitude the craft had when the
|
||||
* pilot let go: altitude error sets a gentle climb rate, climb rate error sets a nose attitude (never more than
|
||||
* {@link #MAX_ATTITUDE} from level), attitude error sets a pitch rate. Bounding the attitude matters: in Warium's
|
||||
* physics the flight path follows the nose only slowly, so steering the flight path directly with pitch rate keeps
|
||||
* 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.
|
||||
* Attitude is measured from the ship's own up axis, so it assumes the craft is built level, as Warium craft are. The
|
||||
* trim's effect is measured every substep (torque per unit of pitch input), which gives the pitch axis and lets the
|
||||
* inner gains be expressed as angular acceleration, so the same tuning works for any craft, speed and altitude.
|
||||
* The trim holds still while the pilot pitches, when too slow, and when wings stall, so it never fights maneuvers.
|
||||
*/
|
||||
final class AutoTrim {
|
||||
// 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
|
||||
private static final double ATTITUDE_GAIN = 1.0;
|
||||
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 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() {
|
||||
}
|
||||
@@ -62,83 +49,57 @@ final class AutoTrim {
|
||||
}
|
||||
|
||||
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;
|
||||
boolean aimed = CursorAimRegistry.isActive(node.key());
|
||||
if (!node.autoTrim()) {
|
||||
state.trimIntegral = 0.0;
|
||||
state.trimOutput = AeroState.moveTowards(state.trimOutput, 0.0, TRIM_RATE * dt);
|
||||
state.savedTrim = 0.0;
|
||||
state.computerOutput = state.pitch + state.trimOutput;
|
||||
targets.altitude = Double.NaN;
|
||||
targets.attitudeIntegral = 0.0;
|
||||
return;
|
||||
}
|
||||
double authority = Config.TRIM_AUTHORITY.get();
|
||||
double response = state.pitchResponse.length();
|
||||
double speed = worldVelocity.length();
|
||||
if (aimed || speed < Config.TRIM_MIN_SPEED.get() || response < 1.0E-6) {
|
||||
// Cursor aim, no airflow or no pitch surfaces: the computer holds what it has
|
||||
targets.altitude = Double.NaN;
|
||||
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)) {
|
||||
boolean hold = Math.abs(node.pitch()) > PILOT_DEADBAND || speed < Config.TRIM_MIN_SPEED.get() || stalled
|
||||
|| response < 1.0E-6;
|
||||
if (hold || Double.isNaN(targets.altitude)) {
|
||||
// Hold whatever altitude the craft is at once the pilot lets go
|
||||
targets.altitude = altitude;
|
||||
targets.attitudeIntegral = 0.0;
|
||||
rateCommand = pilotActive
|
||||
? pilot * Math.toRadians(Config.FBW_STICK_RATE_DEGREES.get()) * (stalled ? STALLED_COMMAND : 1.0)
|
||||
: 0.0;
|
||||
} else {
|
||||
if (Double.isNaN(targets.altitude)) {
|
||||
targets.altitude = altitude;
|
||||
}
|
||||
|
||||
double target = state.trimIntegral;
|
||||
if (!hold) {
|
||||
// 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);
|
||||
if (noseUp == 0.0 || Double.isNaN(attitude)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double climbTarget = clamp(ALTITUDE_GAIN * (targets.altitude - altitude), MAX_CLIMB_RATE);
|
||||
double climbError = climbTarget - worldVelocity.y();
|
||||
targets.attitudeIntegral = clamp(targets.attitudeIntegral + CLIMB_INTEGRAL_GAIN * climbError * dt, MAX_ATTITUDE);
|
||||
// Nose elevation wanted, turned into rotation about the axis
|
||||
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);
|
||||
}
|
||||
|
||||
// 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.trimOutput = AeroState.moveTowards(state.trimOutput, target, TRIM_RATE * dt);
|
||||
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
|
||||
* 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()}) {
|
||||
BlockEntity seat = level.getBlockEntity(pos);
|
||||
if (seat == null || !seat.getPersistentData().contains("ControlX")) {
|
||||
|
||||
@@ -105,7 +105,7 @@ final class CursorPilot {
|
||||
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());
|
||||
node.throttle(), node.autoTrim(), node.storedTrim());
|
||||
}
|
||||
|
||||
/** Least squares for the three inputs whose responses add up to the torque, ridge-regularized so unused axes stay at zero. */
|
||||
|
||||
@@ -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);
|
||||
// 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;
|
||||
// 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
|
||||
private static final double REFERENCE_ANGLE = Math.toRadians(4.0);
|
||||
private static final double REFERENCE_SPEED = 40.0;
|
||||
@@ -68,11 +62,10 @@ final class ImprovedAero {
|
||||
* @param w ship angular velocity, 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 flap flap deflection in radians, 0 for surfaces that are not flaps
|
||||
* @param stalled whether the surface was stalled in the previous substep
|
||||
* @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) {
|
||||
// Airflow at the block: ship velocity plus w x r
|
||||
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
|
||||
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));
|
||||
|
||||
// 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;
|
||||
double critical = Math.toRadians(baseStall) * (1.0 + tuning.vortexStallBonus() * vortexFactor) * tuning.stallAngleScale();
|
||||
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
|
||||
if (!stalled && absAlpha > critical + STALL_BLEND) {
|
||||
stalled = true;
|
||||
@@ -170,7 +156,7 @@ final class ImprovedAero {
|
||||
double dy = -(vn * ny) / planeSpeed;
|
||||
double dz = -(u * cz + vn * nz) / planeSpeed;
|
||||
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);
|
||||
return stalled;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.Set;
|
||||
*/
|
||||
public final class MixinPlugin implements IMixinConfigPlugin {
|
||||
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 boolean warium;
|
||||
|
||||
@@ -5,7 +5,6 @@ package dev.lumareth.improvedAerodynamics.flight;
|
||||
* trim state.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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'
|
||||
* 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
|
||||
@@ -31,8 +31,7 @@ final class WariumAero {
|
||||
switch (s.type()) {
|
||||
case WING -> {
|
||||
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.hypot(v[0], v[2]) * 160.0 * p, 0.0);
|
||||
out.addForce(r, 0.0, Math.abs(fx * v[0] + fz * v[2]) * 160.0 * p, 0.0);
|
||||
double damping = speed * 600.0 * p / m;
|
||||
out.addTorque(-w[0] * damping, -0.1 * w[1] * damping, -w[2] * damping);
|
||||
double drag = -speed * p / m;
|
||||
|
||||
@@ -31,9 +31,6 @@ import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
||||
public final class WariumCapture {
|
||||
public static final String AUTO_TRIM_TAG = "improved_aerodynamics_autotrim";
|
||||
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;
|
||||
|
||||
// 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 key2 = "Null";
|
||||
double brakeInput = 0.0;
|
||||
boolean flap = false;
|
||||
if (type.controllable || type == SurfaceType.AIR_BRAKE) {
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity != null) {
|
||||
CompoundTag tag = blockEntity.getPersistentData();
|
||||
flap = type == SurfaceType.CONTROL_SURFACE && tag.getBoolean(FLAP_TAG);
|
||||
key = tag.getString("Key");
|
||||
key2 = type == SurfaceType.AIR_BRAKE ? "Null" : tag.getString("Key2");
|
||||
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,
|
||||
facing.getStepX(), facing.getStepZ(), pressure, fluidFactor(level), speedMultiplier,
|
||||
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. */
|
||||
@@ -124,8 +119,7 @@ public final class WariumCapture {
|
||||
}
|
||||
}
|
||||
aero.addNode(new NodeData(nodeKey, tag.getDouble("Pitch"), tag.getDouble("Yaw"), tag.getDouble("Roll"),
|
||||
tag.getDouble("Throttle"), autoTrim, storedTrim,
|
||||
FlapSetting.of(tag.getInt(FLAP_SETTING_TAG)).ordinal()));
|
||||
tag.getDouble("Throttle"), autoTrim, storedTrim));
|
||||
}
|
||||
|
||||
/** Raw node value for a key like "Pitch+", as Warium's air brakes read it. */
|
||||
|
||||
-33
@@ -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);
|
||||
}
|
||||
}
|
||||
-33
@@ -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);
|
||||
}
|
||||
}
|
||||
-33
@@ -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,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,16 +23,6 @@ public final class Network {
|
||||
.decoder(ToggleAutoTrimPacket::decode)
|
||||
.consumerMainThread(ToggleAutoTrimPacket::handle)
|
||||
.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)
|
||||
@@ -44,11 +34,5 @@ public final class Network {
|
||||
.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.List;
|
||||
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
|
||||
* 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.
|
||||
* 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.
|
||||
* <p>
|
||||
* 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) {
|
||||
int budget = Config.MAX_NEW_CHUNKS_PER_TICK.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) {
|
||||
Placeholder placeholder = placeholders.get(chunk);
|
||||
if (placeholder != null) {
|
||||
@@ -94,8 +90,7 @@ final class AirPlaceholder {
|
||||
if (budget-- <= 0) {
|
||||
break;
|
||||
}
|
||||
int minSectionY = airHeight.isPresent() ? Math.floorDiv(airHeight.getAsInt() + 15, 16)
|
||||
: Math.floorDiv(estimateSurface(level, pos) + surfaceMargin, 16) + 1;
|
||||
int minSectionY = Math.floorDiv(estimateSurface(level, pos) + surfaceMargin, 16) + 1;
|
||||
int maxSectionY = level.getMaxSection() - 1;
|
||||
if (minSectionY > maxSectionY) {
|
||||
continue;
|
||||
|
||||
@@ -45,11 +45,3 @@ mandatory = false
|
||||
versionRange = "[0.0.4,)"
|
||||
ordering = "AFTER"
|
||||
side = "BOTH"
|
||||
|
||||
# Optional, animated block models
|
||||
[[dependencies."${mod_id}"]]
|
||||
modId = "azurelib"
|
||||
mandatory = false
|
||||
versionRange = "[3.2.7,)"
|
||||
ordering = "AFTER"
|
||||
side = "BOTH"
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.model.nozzle": {
|
||||
"loop": true
|
||||
}
|
||||
}
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "improved_aerodynamics:block/vectored_afterburner_nozzle"
|
||||
}
|
||||
}
|
||||
}
|
||||
-113
@@ -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.improved_aerodynamics.toggle_auto_trim": "Automatische Trimmung umschalten",
|
||||
"message.improved_aerodynamics.auto_trim.on": "Flugcomputer an (automatische Trimmung)",
|
||||
"message.improved_aerodynamics.auto_trim.off": "Flugcomputer aus (automatische Trimmung)",
|
||||
"message.improved_aerodynamics.auto_trim.on": "Automatische Trimmung an",
|
||||
"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.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",
|
||||
@@ -13,30 +13,9 @@
|
||||
"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.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"
|
||||
"message.improved_aerodynamics.cursor_aim.no_server": "Dieser Server hat Improved Aerodynamics nicht, die Cursor-Steuerung braucht es"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"key.categories.improved_aerodynamics": "Improved Aerodynamics",
|
||||
"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.off": "Flight computer off (auto trim)",
|
||||
"message.improved_aerodynamics.auto_trim.on": "Auto trim on",
|
||||
"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.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",
|
||||
@@ -13,30 +13,9 @@
|
||||
"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.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"
|
||||
"message.improved_aerodynamics.cursor_aim.no_server": "This server does not have Improved Aerodynamics, cursor aim needs it"
|
||||
}
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"textures": {
|
||||
"particle": "minecraft:block/iron_block"
|
||||
}
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"parent": "minecraft:block/cube_all",
|
||||
"textures": {
|
||||
"all": "minecraft:block/iron_block"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
-20
@@ -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": [
|
||||
"futurism.ForceInducedShipsMixin",
|
||||
"warium.AirBrakeTickProcedureMixin",
|
||||
"warium.BypassedCompressorScriptProcedureMixin",
|
||||
"warium.ControlSurfaceTickProcedureMixin",
|
||||
"warium.ForceInducedShipsMixin",
|
||||
"warium.HorizontalStabilizerScriptProcedureMixin",
|
||||
"warium.HorizontalWingScriptProcedureMixin",
|
||||
"warium.JetExhaustOnTickUpdateProcedureMixin",
|
||||
"warium.VTOLExhaustTickProcedureMixin",
|
||||
"warium.VerticalAirBrakeTickProcedureMixin",
|
||||
"warium.VerticalControlSurfaceTickProcedureMixin",
|
||||
"warium.VerticalWingScriptProcedureMixin"
|
||||
|
||||
Reference in new issue
Block a user