Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70eb237d70 |
No files matched your search
+12
-2
@@ -58,6 +58,13 @@ repositories {
|
|||||||
includeGroup "maven.modrinth"
|
includeGroup "maven.modrinth"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
maven {
|
||||||
|
name = "AzureDoom Mods"
|
||||||
|
url = "https://maven.azuredoom.com/mods"
|
||||||
|
content {
|
||||||
|
includeGroup "mod.azure.azurelib"
|
||||||
|
}
|
||||||
|
}
|
||||||
// Warium VS is not on Modrinth, it is compiled against the jar in libs/
|
// Warium VS is not on Modrinth, it is compiled against the jar in libs/
|
||||||
flatDir {
|
flatDir {
|
||||||
dir 'libs'
|
dir 'libs'
|
||||||
@@ -73,9 +80,12 @@ dependencies {
|
|||||||
compileOnly files('libs/vs-core-api-1.1.0.jar', 'libs/vs-core-util-1.1.0.jar', 'libs/joml-primitives-1.10.0.jar')
|
compileOnly files('libs/vs-core-api-1.1.0.jar', 'libs/vs-core-util-1.1.0.jar', 'libs/joml-primitives-1.10.0.jar')
|
||||||
compileOnly 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
|
compileOnly 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
|
||||||
|
|
||||||
// Warium VS 1.1.6 and Futurism 0.0.4 (optional dependencies, the flight physics mixins only apply when they are present)
|
// 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.6")
|
compileOnly fg.deobf("blank:WariumVS:1.1.7")
|
||||||
compileOnly fg.deobf("blank:futurism:0.0.4")
|
compileOnly fg.deobf("blank:futurism:0.0.4")
|
||||||
|
|
||||||
|
// AzureLib 3.2.7 for the animated block models (optional dependency, the common jar uses obfuscated names, so it is remapped)
|
||||||
|
compileOnly fg.deobf("mod.azure.azurelib:azurelib-common-${minecraft_version}:${azurelib_version}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ mod_id=improved_aerodynamics
|
|||||||
mod_name=VS: Improved Aerodynamics
|
mod_name=VS: Improved Aerodynamics
|
||||||
mod_license=MIT
|
mod_license=MIT
|
||||||
mod_version=0.3.0
|
mod_version=0.3.0
|
||||||
|
azurelib_version=3.2.7
|
||||||
mod_group_id=dev.lumareth.improvedaerodynamics
|
mod_group_id=dev.lumareth.improvedaerodynamics
|
||||||
mod_authors=Lumareth
|
mod_authors=Lumareth
|
||||||
mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships and smooths out Warium flight physics, with an optional flight model with real stalls and auto trim.
|
mod_description=Valkyrien Skies addon that keeps terrain streaming ahead of fast ships and smooths out Warium flight physics, with an optional flight model with real stalls and auto trim.
|
||||||
Binary file not shown.
@@ -131,12 +131,58 @@ public final class Config {
|
|||||||
.comment("Auto trim: below this airspeed (blocks per second) the trim holds its current value")
|
.comment("Auto trim: below this airspeed (blocks per second) the trim holds its current value")
|
||||||
.defineInRange("trimMinSpeed", 10.0D, 0.0D, 100.0D);
|
.defineInRange("trimMinSpeed", 10.0D, 0.0D, 100.0D);
|
||||||
|
|
||||||
|
static {
|
||||||
|
BUILDER.pop();
|
||||||
|
BUILDER.comment("Vectored afterburner nozzle block").push("nozzle");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue NOZZLE_MAX_DEFLECTION = BUILDER
|
||||||
|
.comment("Largest angle in degrees the nozzle can swing to either side")
|
||||||
|
.defineInRange("maxDeflectionDegrees", 20.0D, 1.0D, 45.0D);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue NOZZLE_SLEW_RATE = BUILDER
|
||||||
|
.comment("Fastest the nozzle swings, in degrees per second, so it never snaps to a new angle")
|
||||||
|
.defineInRange("slewRateDegreesPerSecond", 90.0D, 5.0D, 720.0D);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue AFTERBURNER_MIN_THROTTLE = BUILDER
|
||||||
|
.comment("Lowest throttle value on the vehicle control node (0 to 10) at which the afterburner key lights the",
|
||||||
|
"afterburner. It goes out by itself when the throttle falls below it")
|
||||||
|
.defineInRange("afterburnerMinThrottle", 8.0D, 0.0D, 10.0D);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.ConfigValue<String> EXHAUST_STYLE = BUILDER
|
||||||
|
.comment("How every jet exhaust looks: Warium's jet exhaust and VTOL exhaust, Futurism's vectored afterburner and",
|
||||||
|
"this mod's nozzle. wariumvs for Warium's own effect entities, improved for the big flame, particles and",
|
||||||
|
"afterburner sound of this mod. Warium's engine sound is kept in both")
|
||||||
|
.defineInList("exhaustStyle", "improved", java.util.List.of("wariumvs", "improved"));
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.DoubleValue NOZZLE_YAW_MIX = BUILDER
|
||||||
|
.comment("How much of the yaw input goes into the nozzles' differential deflection. The nozzles are canted, so",
|
||||||
|
"deflecting them against each other pushes the tail sideways as well as rolling the craft, like on an",
|
||||||
|
"Su-57. 0 turns yaw vectoring off, a negative value turns it around")
|
||||||
|
.defineInRange("yawMix", 0.6D, -1.0D, 1.0D);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.BooleanValue NOZZLE_LIGHT = BUILDER
|
||||||
|
.comment("The nozzle gives off light while its afterburner is lit")
|
||||||
|
.define("lightEnabled", true);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
BUILDER.pop();
|
BUILDER.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final ForgeConfigSpec SPEC = BUILDER.build();
|
static final ForgeConfigSpec SPEC = BUILDER.build();
|
||||||
|
|
||||||
|
private static final ForgeConfigSpec.Builder CLIENT_BUILDER = new ForgeConfigSpec.Builder();
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.BooleanValue NOZZLE_PARTICLES = CLIENT_BUILDER
|
||||||
|
.comment("Jet exhaust in the improved style: smoke at dry thrust and fire in afterburner")
|
||||||
|
.define("particlesEnabled", true);
|
||||||
|
|
||||||
|
public static final ForgeConfigSpec.BooleanValue NOZZLE_SOUND = CLIENT_BUILDER
|
||||||
|
.comment("Vectored afterburner nozzle: afterburner ignition and heavy afterburner sound")
|
||||||
|
.define("soundEnabled", true);
|
||||||
|
|
||||||
|
static final ForgeConfigSpec CLIENT_SPEC = CLIENT_BUILDER.build();
|
||||||
|
|
||||||
private Config() {
|
private Config() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,13 @@ package dev.lumareth.improvedAerodynamics;
|
|||||||
import com.mojang.brigadier.arguments.ArgumentType;
|
import com.mojang.brigadier.arguments.ArgumentType;
|
||||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||||
import dev.lumareth.improvedAerodynamics.flight.ControlMode;
|
import dev.lumareth.improvedAerodynamics.exhaust.NozzleCommands;
|
||||||
import net.minecraft.commands.CommandSourceStack;
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
import net.minecraft.commands.Commands;
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.commands.SharedSuggestionProvider;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraftforge.common.ForgeConfigSpec;
|
import net.minecraftforge.common.ForgeConfigSpec;
|
||||||
@@ -19,12 +21,15 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* /improvedaerodynamics (or /aero) to read and change the most important server config values in game.
|
* /improvedaerodynamics (or /aero) to read and change the most important server config values in game.
|
||||||
* {@code /aero} lists them, {@code /aero <option>} shows one, {@code /aero <option> <value>} sets and saves it.
|
* {@code /aero} lists them, {@code /aero <option>} shows one, {@code /aero <option> <value>} sets and saves it.
|
||||||
* The config options need operator rights, except for the owner of a singleplayer world, who may use them without cheats.
|
* Needs operator rights, except for the owner of a singleplayer world, who may use them without cheats.
|
||||||
* {@code /aero controls keyboard|cursor_aim} is open to every player and picks how they fly.
|
|
||||||
* All of them are read live, so changes apply immediately.
|
* All of them are read live, so changes apply immediately.
|
||||||
*/
|
*/
|
||||||
public final class ConfigCommands {
|
public final class ConfigCommands {
|
||||||
private record Option<T>(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type) {
|
private record Option<T>(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type,
|
||||||
|
List<String> choices) {
|
||||||
|
Option(String name, ForgeConfigSpec.ConfigValue<T> value, ArgumentType<T> argument, Class<T> type) {
|
||||||
|
this(name, value, argument, type, List.of());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final List<Option<?>> OPTIONS = List.of(
|
private static final List<Option<?>> OPTIONS = List.of(
|
||||||
@@ -46,6 +51,12 @@ public final class ConfigCommands {
|
|||||||
new Option<>("trim_authority", Config.TRIM_AUTHORITY, 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<>("trim_min_speed", Config.TRIM_MIN_SPEED, DoubleArgumentType.doubleArg(), Double.class),
|
||||||
new Option<>("air_placeholder", Config.ENABLE_AIR_PLACEHOLDER, BoolArgumentType.bool(), Boolean.class),
|
new Option<>("air_placeholder", Config.ENABLE_AIR_PLACEHOLDER, BoolArgumentType.bool(), Boolean.class),
|
||||||
|
new Option<>("nozzle_max_deflection", Config.NOZZLE_MAX_DEFLECTION, DoubleArgumentType.doubleArg(), Double.class),
|
||||||
|
new Option<>("nozzle_slew_rate", Config.NOZZLE_SLEW_RATE, DoubleArgumentType.doubleArg(), Double.class),
|
||||||
|
new Option<>("afterburner_min_throttle", Config.AFTERBURNER_MIN_THROTTLE, DoubleArgumentType.doubleArg(), Double.class),
|
||||||
|
new Option<>("exhaust_style", Config.EXHAUST_STYLE, StringArgumentType.word(), String.class, List.of("wariumvs", "improved")),
|
||||||
|
new Option<>("nozzle_yaw_mix", Config.NOZZLE_YAW_MIX, DoubleArgumentType.doubleArg(), Double.class),
|
||||||
|
new Option<>("nozzle_light", Config.NOZZLE_LIGHT, BoolArgumentType.bool(), Boolean.class),
|
||||||
new Option<>("lookahead_seconds", Config.LOOKAHEAD_SECONDS, DoubleArgumentType.doubleArg(), Double.class));
|
new Option<>("lookahead_seconds", Config.LOOKAHEAD_SECONDS, DoubleArgumentType.doubleArg(), Double.class));
|
||||||
|
|
||||||
private ConfigCommands() {
|
private ConfigCommands() {
|
||||||
@@ -62,52 +73,30 @@ public final class ConfigCommands {
|
|||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void onRegisterCommands(RegisterCommandsEvent event) {
|
public static void onRegisterCommands(RegisterCommandsEvent event) {
|
||||||
// The root is open to everybody for /aero controls, the config options check for rights themselves
|
|
||||||
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics")
|
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("improvedaerodynamics")
|
||||||
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource()));
|
.requires(ConfigCommands::mayUse)
|
||||||
|
.executes(context -> list(context.getSource()));
|
||||||
for (Option<?> option : OPTIONS) {
|
for (Option<?> option : OPTIONS) {
|
||||||
root.then(optionNode(option).requires(ConfigCommands::mayUse));
|
root.then(optionNode(option));
|
||||||
}
|
}
|
||||||
root.then(controlsNode());
|
root.then(SeedgenCommands.node());
|
||||||
|
root.then(NozzleCommands.node());
|
||||||
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
|
LiteralCommandNode<CommandSourceStack> node = event.getDispatcher().register(root);
|
||||||
event.getDispatcher().register(Commands.literal("aero")
|
event.getDispatcher().register(Commands.literal("aero")
|
||||||
.executes(context -> mayUse(context.getSource()) ? list(context.getSource()) : denied(context.getSource()))
|
.requires(ConfigCommands::mayUse)
|
||||||
|
.executes(context -> list(context.getSource()))
|
||||||
.redirect(node));
|
.redirect(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int denied(CommandSourceStack source) {
|
|
||||||
source.sendFailure(Component.literal("Use /aero controls keyboard or /aero controls cursor_aim"));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** /aero controls [keyboard|cursor_aim], for every player (permission level 0). */
|
|
||||||
private static LiteralArgumentBuilder<CommandSourceStack> controlsNode() {
|
|
||||||
return Commands.literal("controls")
|
|
||||||
.requires(source -> source.getEntity() instanceof ServerPlayer)
|
|
||||||
.executes(context -> {
|
|
||||||
ServerPlayer player = context.getSource().getPlayerOrException();
|
|
||||||
boolean cursorAim = ControlMode.isCursorAim(player);
|
|
||||||
context.getSource().sendSuccess(() -> Component.literal("Controls: " + (cursorAim ? "cursor_aim" : "keyboard")), false);
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
.then(Commands.literal("keyboard").executes(context -> setControls(context.getSource(), false)))
|
|
||||||
.then(Commands.literal("cursor_aim").executes(context -> setControls(context.getSource(), true)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int setControls(CommandSourceStack source, boolean cursorAim) throws com.mojang.brigadier.exceptions.CommandSyntaxException {
|
|
||||||
ServerPlayer player = source.getPlayerOrException();
|
|
||||||
ControlMode.set(player, cursorAim);
|
|
||||||
source.sendSuccess(() -> Component.literal("Controls set to " + (cursorAim ? "cursor_aim" : "keyboard")), false);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> LiteralArgumentBuilder<CommandSourceStack> optionNode(Option<T> option) {
|
private static <T> LiteralArgumentBuilder<CommandSourceStack> optionNode(Option<T> option) {
|
||||||
return Commands.literal(option.name())
|
return Commands.literal(option.name())
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get()), false);
|
context.getSource().sendSuccess(() -> Component.literal(option.name() + " = " + option.value().get()
|
||||||
|
+ (option.choices().isEmpty() ? "" : " (" + String.join(" or ", option.choices()) + ")")), false);
|
||||||
return 1;
|
return 1;
|
||||||
})
|
})
|
||||||
.then(Commands.argument("value", option.argument())
|
.then(Commands.argument("value", option.argument())
|
||||||
|
.suggests((context, builder) -> SharedSuggestionProvider.suggest(option.choices(), builder))
|
||||||
.executes(context -> set(context.getSource(), option, context.getArgument("value", option.type()))));
|
.executes(context -> set(context.getSource(), option, context.getArgument("value", option.type()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +106,8 @@ public final class ConfigCommands {
|
|||||||
ForgeConfigSpec.Range<?> range = spec.getRange();
|
ForgeConfigSpec.Range<?> range = spec.getRange();
|
||||||
source.sendFailure(Component.literal(range != null
|
source.sendFailure(Component.literal(range != null
|
||||||
? option.name() + " must be between " + range.getMin() + " and " + range.getMax()
|
? option.name() + " must be between " + range.getMin() + " and " + range.getMax()
|
||||||
: "Invalid value for " + option.name()));
|
: option.choices().isEmpty() ? "Invalid value for " + option.name()
|
||||||
|
: option.name() + " must be " + String.join(" or ", option.choices())));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
option.value().set(newValue);
|
option.value().set(newValue);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package dev.lumareth.improvedAerodynamics;
|
package dev.lumareth.improvedAerodynamics;
|
||||||
|
|
||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
import dev.lumareth.improvedAerodynamics.client.aim.CursorAimClientConfig;
|
import dev.lumareth.improvedAerodynamics.exhaust.ExhaustSync;
|
||||||
import dev.lumareth.improvedAerodynamics.client.pipper.PipperConfig;
|
import dev.lumareth.improvedAerodynamics.exhaust.NozzleContent;
|
||||||
import dev.lumareth.improvedAerodynamics.flight.CursorAimConfig;
|
|
||||||
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
import dev.lumareth.improvedAerodynamics.flight.PhysicsBridge;
|
||||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||||
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
import dev.lumareth.improvedAerodynamics.terrain.LookaheadLoader;
|
||||||
@@ -11,6 +10,7 @@ import net.minecraftforge.common.MinecraftForge;
|
|||||||
import net.minecraftforge.fml.ModLoadingContext;
|
import net.minecraftforge.fml.ModLoadingContext;
|
||||||
import net.minecraftforge.fml.common.Mod;
|
import net.minecraftforge.fml.common.Mod;
|
||||||
import net.minecraftforge.fml.config.ModConfig;
|
import net.minecraftforge.fml.config.ModConfig;
|
||||||
|
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
@Mod(ImprovedAerodynamics.MOD_ID)
|
@Mod(ImprovedAerodynamics.MOD_ID)
|
||||||
@@ -23,11 +23,10 @@ public class ImprovedAerodynamics {
|
|||||||
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
MinecraftForge.EVENT_BUS.register(LookaheadLoader.class);
|
||||||
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
MinecraftForge.EVENT_BUS.register(PhysicsBridge.class);
|
||||||
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
|
MinecraftForge.EVENT_BUS.register(ConfigCommands.class);
|
||||||
MinecraftForge.EVENT_BUS.register(dev.lumareth.improvedAerodynamics.flight.ControlMode.class);
|
MinecraftForge.EVENT_BUS.register(ExhaustSync.class);
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, Config.SPEC);
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, PipperConfig.SPEC);
|
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.CLIENT_SPEC);
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, CursorAimConfig.SPEC, "improved_aerodynamics-cursor-aim.toml");
|
NozzleContent.register(FMLJavaModLoadingContext.get().getModEventBus());
|
||||||
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, CursorAimClientConfig.SPEC, "improved_aerodynamics-cursor-aim-client.toml");
|
|
||||||
Network.register();
|
Network.register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package dev.lumareth.improvedAerodynamics.client;
|
|||||||
import com.mojang.blaze3d.platform.InputConstants;
|
import com.mojang.blaze3d.platform.InputConstants;
|
||||||
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
||||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
import dev.lumareth.improvedAerodynamics.network.Network;
|
||||||
|
import dev.lumareth.improvedAerodynamics.network.ToggleAfterburnerPacket;
|
||||||
import dev.lumareth.improvedAerodynamics.network.ToggleAutoTrimPacket;
|
import dev.lumareth.improvedAerodynamics.network.ToggleAutoTrimPacket;
|
||||||
import net.minecraft.client.KeyMapping;
|
import net.minecraft.client.KeyMapping;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
@@ -18,6 +19,9 @@ public final class KeyBindings {
|
|||||||
public static final KeyMapping TOGGLE_AUTO_TRIM = new KeyMapping("key.improved_aerodynamics.toggle_auto_trim",
|
public static final KeyMapping TOGGLE_AUTO_TRIM = new KeyMapping("key.improved_aerodynamics.toggle_auto_trim",
|
||||||
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, "key.categories.improved_aerodynamics");
|
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, "key.categories.improved_aerodynamics");
|
||||||
|
|
||||||
|
public static final KeyMapping TOGGLE_AFTERBURNER = new KeyMapping("key.improved_aerodynamics.toggle_afterburner",
|
||||||
|
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_K, "key.categories.improved_aerodynamics");
|
||||||
|
|
||||||
private KeyBindings() {
|
private KeyBindings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +33,7 @@ public final class KeyBindings {
|
|||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
|
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
|
||||||
event.register(TOGGLE_AUTO_TRIM);
|
event.register(TOGGLE_AUTO_TRIM);
|
||||||
|
event.register(TOGGLE_AFTERBURNER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +55,12 @@ public final class KeyBindings {
|
|||||||
Network.CHANNEL.sendToServer(new ToggleAutoTrimPacket());
|
Network.CHANNEL.sendToServer(new ToggleAutoTrimPacket());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
while (TOGGLE_AFTERBURNER.consumeClick()) {
|
||||||
|
if (minecraft.player != null && minecraft.player.getVehicle() != null && minecraft.getConnection() != null
|
||||||
|
&& Network.CHANNEL.isRemotePresent(minecraft.getConnection().getConnection())) {
|
||||||
|
Network.CHANNEL.sendToServer(new ToggleAfterburnerPacket());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
|
||||||
|
|
||||||
import com.mojang.blaze3d.platform.InputConstants;
|
|
||||||
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
|
||||||
import net.minecraft.client.KeyMapping;
|
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
|
||||||
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
|
|
||||||
import net.minecraftforge.client.settings.KeyConflictContext;
|
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
|
||||||
import net.minecraftforge.fml.common.Mod;
|
|
||||||
import org.lwjgl.glfw.GLFW;
|
|
||||||
|
|
||||||
/** Keys of cursor aim, which is switched on with /aero controls cursor_aim. The free look key is not shift: in a seat, shift gets the player out of it. */
|
|
||||||
public final class AimKeys {
|
|
||||||
public static final KeyMapping FREE_LOOK = new KeyMapping("key.improved_aerodynamics.cursor_aim_free_look",
|
|
||||||
KeyConflictContext.IN_GAME, InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_LEFT_ALT, "key.categories.improved_aerodynamics");
|
|
||||||
|
|
||||||
private AimKeys() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
|
|
||||||
public static final class Registration {
|
|
||||||
private Registration() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
|
|
||||||
event.register(FREE_LOOK);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
|
||||||
|
|
||||||
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
|
|
||||||
/** Everything that touches Warium VS classes, so nothing else loads them when Warium is missing. */
|
|
||||||
final class AimSeat {
|
|
||||||
private AimSeat() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static Entity seatOf(Entity rider) {
|
|
||||||
Entity root = rider.getRootVehicle();
|
|
||||||
return root instanceof SeatEntityEntity ? root : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
|
||||||
|
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
|
||||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
|
||||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
|
||||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
|
||||||
import com.mojang.blaze3d.vertex.Tesselator;
|
|
||||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
|
||||||
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
|
||||||
import dev.lumareth.improvedAerodynamics.client.pipper.GunPipperRenderer;
|
|
||||||
import dev.lumareth.improvedAerodynamics.flight.AimBasis;
|
|
||||||
import dev.lumareth.improvedAerodynamics.network.AimPacket;
|
|
||||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
|
||||||
import net.minecraft.client.Camera;
|
|
||||||
import net.minecraft.client.CameraType;
|
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.client.multiplayer.ClientLevel;
|
|
||||||
import net.minecraft.client.player.LocalPlayer;
|
|
||||||
import net.minecraft.client.renderer.GameRenderer;
|
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.network.chat.Component;
|
|
||||||
import net.minecraft.util.Mth;
|
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
import net.minecraft.world.phys.Vec3;
|
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
|
||||||
import net.minecraftforge.client.event.InputEvent;
|
|
||||||
import net.minecraftforge.client.event.RenderLevelStageEvent;
|
|
||||||
import net.minecraftforge.client.event.ViewportEvent;
|
|
||||||
import net.minecraftforge.event.TickEvent;
|
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
|
||||||
import net.minecraftforge.fml.ModList;
|
|
||||||
import net.minecraftforge.fml.common.Mod;
|
|
||||||
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
|
|
||||||
import org.joml.Matrix4f;
|
|
||||||
import org.joml.Quaterniondc;
|
|
||||||
import org.joml.Vector3d;
|
|
||||||
import org.joml.Vector3f;
|
|
||||||
import org.valkyrienskies.core.api.ships.ClientShip;
|
|
||||||
import org.valkyrienskies.core.api.ships.Ship;
|
|
||||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cursor aim, the client side. While it is on:
|
|
||||||
* <ul>
|
|
||||||
* <li>the mouse moves a cursor instead of the camera. The cursor is a direction relative to the gun reticle (the
|
|
||||||
* craft's boresight), at most {@code maxCursorAngleDegrees} away from it, and springs back to it when the mouse
|
|
||||||
* rests. It is sent to the server every tick, where a virtual pilot flies the craft by it;</li>
|
|
||||||
* <li>the camera is a chase view, looking along the reticle, or, with look ahead, towards the cursor;</li>
|
|
||||||
* <li>holding the free look key moves the camera instead of the cursor, it eases back when released;</li>
|
|
||||||
* <li>the mouse wheel changes how far the camera is behind the craft.</li>
|
|
||||||
* </ul>
|
|
||||||
* The camera is driven through the rotation of the player, which for a rider of a Valkyrien Skies ship is relative to
|
|
||||||
* the ship, so the camera follows the ship's attitude like Valkyrien Skies does for any mounted player. Mouse
|
|
||||||
* movement shows up as a change of that rotation, which is taken as the movement of the cursor and put back.
|
|
||||||
*/
|
|
||||||
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
|
|
||||||
public final class CursorAimClient {
|
|
||||||
private static final int CURSOR_COLOR = 0xFFFFE040;
|
|
||||||
private static final int RETICLE_COLOR = 0xFFFFFFFF;
|
|
||||||
private static final float MARKER_DISTANCE = 2.0F;
|
|
||||||
private static final double LOOK_AHEAD_RATE = 6.0;
|
|
||||||
private static final double FREE_LOOK_RETURN_RATE = 10.0;
|
|
||||||
|
|
||||||
private static boolean active;
|
|
||||||
// Chosen with /aero controls, told by the server
|
|
||||||
private static boolean cursorAimMode;
|
|
||||||
// Cursor as components along the craft's forward (gun reticle), right and up axes
|
|
||||||
private static double cf = 1.0;
|
|
||||||
private static double cr;
|
|
||||||
private static double cu;
|
|
||||||
// Camera direction in ship space and the free look offset on top of it, degrees
|
|
||||||
private static double[] cameraDirection;
|
|
||||||
private static double offsetYaw;
|
|
||||||
private static double offsetPitch;
|
|
||||||
// Rotation the player was given last frame, to see what the mouse did since
|
|
||||||
private static boolean haveLast;
|
|
||||||
private static float lastYaw;
|
|
||||||
private static float lastPitch;
|
|
||||||
private static long lastNanos;
|
|
||||||
private static double zoom;
|
|
||||||
private static CameraType previousCamera;
|
|
||||||
private static Method cameraMove;
|
|
||||||
|
|
||||||
private CursorAimClient() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Toggling and the packet to the server ----
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onClientTick(TickEvent.ClientTickEvent event) {
|
|
||||||
if (event.phase != TickEvent.Phase.END) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
|
||||||
LocalPlayer player = minecraft.player;
|
|
||||||
if (!active) {
|
|
||||||
if (cursorAimMode && player != null && seated(player) && serverHasMod(minecraft)) {
|
|
||||||
activate(minecraft, player);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!cursorAimMode) {
|
|
||||||
deactivate(minecraft);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (player == null || !seated(player)) {
|
|
||||||
deactivate(minecraft);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
send(minecraft, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called when the server tells the client how its player flies. */
|
|
||||||
public static void setMode(boolean cursorAim) {
|
|
||||||
cursorAimMode = cursorAim;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean seated(LocalPlayer player) {
|
|
||||||
return ModList.get().isLoaded("valkyrien_warium") && AimSeat.seatOf(player) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean serverHasMod(Minecraft minecraft) {
|
|
||||||
return minecraft.getConnection() != null && Network.CHANNEL.isRemotePresent(minecraft.getConnection().getConnection());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void activate(Minecraft minecraft, LocalPlayer player) {
|
|
||||||
if (!seated(player)) {
|
|
||||||
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.no_seat"), true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!serverHasMod(minecraft)) {
|
|
||||||
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.no_server"), true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
active = true;
|
|
||||||
cf = 1.0;
|
|
||||||
cr = 0.0;
|
|
||||||
cu = 0.0;
|
|
||||||
cameraDirection = null;
|
|
||||||
offsetYaw = 0.0;
|
|
||||||
offsetPitch = 0.0;
|
|
||||||
haveLast = false;
|
|
||||||
lastNanos = System.nanoTime();
|
|
||||||
previousCamera = minecraft.options.getCameraType();
|
|
||||||
minecraft.options.setCameraType(CameraType.THIRD_PERSON_BACK);
|
|
||||||
player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.on"), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void deactivate(Minecraft minecraft) {
|
|
||||||
if (!active) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
active = false;
|
|
||||||
send(minecraft, false);
|
|
||||||
if (previousCamera != null) {
|
|
||||||
minecraft.options.setCameraType(previousCamera);
|
|
||||||
previousCamera = null;
|
|
||||||
}
|
|
||||||
if (minecraft.player != null) {
|
|
||||||
minecraft.player.displayClientMessage(Component.translatable("message.improved_aerodynamics.cursor_aim.off"), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void send(Minecraft minecraft, boolean on) {
|
|
||||||
if (serverHasMod(minecraft)) {
|
|
||||||
Network.CHANNEL.sendToServer(new AimPacket(on, (float) cf, (float) cr, (float) cu));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Mouse and camera, once per frame before the world is rendered ----
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onRenderTick(TickEvent.RenderTickEvent event) {
|
|
||||||
if (!active || event.phase != TickEvent.Phase.START) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
|
||||||
LocalPlayer player = minecraft.player;
|
|
||||||
ClientLevel level = minecraft.level;
|
|
||||||
if (player == null || level == null || !seated(player)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Entity seat = AimSeat.seatOf(player);
|
|
||||||
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
|
||||||
if (basis == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (minecraft.options.getCameraType() != CameraType.THIRD_PERSON_BACK) {
|
|
||||||
minecraft.options.setCameraType(CameraType.THIRD_PERSON_BACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = System.nanoTime();
|
|
||||||
double dt = Mth.clamp((now - lastNanos) / 1.0E9, 0.0, 0.1);
|
|
||||||
lastNanos = now;
|
|
||||||
|
|
||||||
// What the mouse did: the player's rotation moved away from what it was set to
|
|
||||||
double deltaYaw = 0.0;
|
|
||||||
double deltaPitch = 0.0;
|
|
||||||
if (haveLast && minecraft.screen == null) {
|
|
||||||
deltaYaw = Mth.wrapDegrees(player.getYRot() - lastYaw);
|
|
||||||
deltaPitch = player.getXRot() - lastPitch;
|
|
||||||
if (Math.abs(deltaYaw) > 90.0 || Math.abs(deltaPitch) > 90.0) {
|
|
||||||
// Not the mouse, something reset the rotation
|
|
||||||
deltaYaw = 0.0;
|
|
||||||
deltaPitch = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double sensitivity = CursorAimClientConfig.MOUSE_SENSITIVITY.get();
|
|
||||||
boolean freeLook = minecraft.screen == null && AimKeys.FREE_LOOK.isDown();
|
|
||||||
|
|
||||||
double spring = CursorAimClientConfig.SPRING_BACK_SECONDS.get();
|
|
||||||
if (spring > 0.0) {
|
|
||||||
shrinkCursor(Math.exp(-dt / spring));
|
|
||||||
}
|
|
||||||
if (freeLook) {
|
|
||||||
offsetYaw += deltaYaw * sensitivity;
|
|
||||||
offsetPitch = Mth.clamp(offsetPitch - deltaPitch * sensitivity, -89.0, 89.0);
|
|
||||||
} else {
|
|
||||||
moveCursor(Math.toRadians(deltaYaw * sensitivity), Math.toRadians(-deltaPitch * sensitivity));
|
|
||||||
double decay = Math.exp(-dt * FREE_LOOK_RETURN_RATE);
|
|
||||||
offsetYaw *= decay;
|
|
||||||
offsetPitch *= decay;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Camera: along the reticle, or turned towards the cursor by the look ahead setting
|
|
||||||
double lookAhead = CursorAimClientConfig.LOOK_AHEAD.get();
|
|
||||||
double[] forward = basis.forward();
|
|
||||||
if (cameraDirection == null || lookAhead <= 0.0) {
|
|
||||||
cameraDirection = forward.clone();
|
|
||||||
} else {
|
|
||||||
double[] target = basis.toShip(cf, cr, cu);
|
|
||||||
cameraDirection = turnTowards(cameraDirection, target, 1.0 - Math.exp(-lookAhead * LOOK_AHEAD_RATE * dt));
|
|
||||||
}
|
|
||||||
double[] direction = rotate(cameraDirection, basis.up(), -Math.toRadians(offsetYaw));
|
|
||||||
direction = rotate(direction, rotate(basis.right(), basis.up(), -Math.toRadians(offsetYaw)), Math.toRadians(offsetPitch));
|
|
||||||
|
|
||||||
// Valkyrien Skies reads the view of a mounted player in ship space: yaw 0 looks along +Z
|
|
||||||
float yaw = (float) Math.toDegrees(Math.atan2(-direction[0], direction[2]));
|
|
||||||
float pitch = (float) Math.toDegrees(Math.atan2(-direction[1], Math.hypot(direction[0], direction[2])));
|
|
||||||
player.setYRot(yaw);
|
|
||||||
player.setXRot(pitch);
|
|
||||||
player.yRotO = yaw;
|
|
||||||
player.xRotO = pitch;
|
|
||||||
player.yHeadRot = yaw;
|
|
||||||
player.yHeadRotO = yaw;
|
|
||||||
lastYaw = yaw;
|
|
||||||
lastPitch = pitch;
|
|
||||||
haveLast = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Moves the cursor like a look direction: yaw towards the right, pitch up, radians. */
|
|
||||||
private static void moveCursor(double yaw, double pitch) {
|
|
||||||
double nf = cf * Math.cos(yaw) - cr * Math.sin(yaw);
|
|
||||||
double nr = cf * Math.sin(yaw) + cr * Math.cos(yaw);
|
|
||||||
cf = nf;
|
|
||||||
cr = nr;
|
|
||||||
nf = cf * Math.cos(pitch) - cu * Math.sin(pitch);
|
|
||||||
double nu = cf * Math.sin(pitch) + cu * Math.cos(pitch);
|
|
||||||
cf = nf;
|
|
||||||
cu = nu;
|
|
||||||
normalizeCursor();
|
|
||||||
limitCursor();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Brings the cursor back towards the reticle along the shortest way, keeping only this fraction of its angle. */
|
|
||||||
private static void shrinkCursor(double keep) {
|
|
||||||
double angle = Math.acos(Mth.clamp(cf, -1.0, 1.0));
|
|
||||||
double lateral = Math.sqrt(cr * cr + cu * cu);
|
|
||||||
if (angle < 1.0E-6 || lateral < 1.0E-9) {
|
|
||||||
cf = 1.0;
|
|
||||||
cr = 0.0;
|
|
||||||
cu = 0.0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double newAngle = angle * keep;
|
|
||||||
double scale = Math.sin(newAngle) / lateral;
|
|
||||||
cf = Math.cos(newAngle);
|
|
||||||
cr *= scale;
|
|
||||||
cu *= scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void normalizeCursor() {
|
|
||||||
double length = Math.sqrt(cf * cf + cr * cr + cu * cu);
|
|
||||||
if (!(length > 1.0E-9)) {
|
|
||||||
cf = 1.0;
|
|
||||||
cr = 0.0;
|
|
||||||
cu = 0.0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cf /= length;
|
|
||||||
cr /= length;
|
|
||||||
cu /= length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void limitCursor() {
|
|
||||||
double limit = Math.toRadians(CursorAimClientConfig.MAX_CURSOR_ANGLE_DEGREES.get());
|
|
||||||
double angle = Math.acos(Mth.clamp(cf, -1.0, 1.0));
|
|
||||||
if (angle <= limit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double lateral = Math.sqrt(cr * cr + cu * cu);
|
|
||||||
if (lateral < 1.0E-9) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double scale = Math.sin(limit) / lateral;
|
|
||||||
cf = Math.cos(limit);
|
|
||||||
cr *= scale;
|
|
||||||
cu *= scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Mouse wheel and camera distance ----
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onScroll(InputEvent.MouseScrollingEvent event) {
|
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
|
||||||
if (!active || minecraft.screen != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
zoom = Mth.clamp(zoom - event.getScrollDelta() * CursorAimClientConfig.ZOOM_STEP.get(), -3.0, CursorAimClientConfig.ZOOM_MAX.get());
|
|
||||||
event.setCanceled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onCameraAngles(ViewportEvent.ComputeCameraAngles event) {
|
|
||||||
if (!active || Math.abs(zoom) < 1.0E-3) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (cameraMove == null) {
|
|
||||||
// Camera.move(double, double, double), moves along the camera's own axes
|
|
||||||
cameraMove = ObfuscationReflectionHelper.findMethod(Camera.class, "m_90568_", double.class, double.class, double.class);
|
|
||||||
}
|
|
||||||
cameraMove.invoke(event.getCamera(), -zoom, 0.0, 0.0);
|
|
||||||
} catch (ReflectiveOperationException | RuntimeException exception) {
|
|
||||||
ImprovedAerodynamics.LOGGER.warn("Cursor aim camera zoom is not available", exception);
|
|
||||||
zoom = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Drawing the cursor ----
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onRenderLevel(RenderLevelStageEvent event) {
|
|
||||||
if (!active || event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
|
||||||
ClientLevel level = minecraft.level;
|
|
||||||
LocalPlayer player = minecraft.player;
|
|
||||||
if (level == null || player == null || !seated(player)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Entity seat = AimSeat.seatOf(player);
|
|
||||||
AimBasis basis = AimBasis.of(level, seat.blockPosition());
|
|
||||||
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, BlockPos.containing(seat.getX(), seat.getY(), seat.getZ()));
|
|
||||||
if (basis == null || ship == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Quaterniondc rotation = ship instanceof ClientShip clientShip
|
|
||||||
? clientShip.getRenderTransform().getShipToWorldRotation() : ship.getTransform().getShipToWorldRotation();
|
|
||||||
double[] cursorShip = basis.toShip(cf, cr, cu);
|
|
||||||
Vector3d cursorWorld = rotation.transform(new Vector3d(cursorShip[0], cursorShip[1], cursorShip[2]));
|
|
||||||
double[] forward = basis.forward();
|
|
||||||
Vector3d reticleWorld = rotation.transform(new Vector3d(forward[0], forward[1], forward[2]));
|
|
||||||
|
|
||||||
Camera camera = event.getCamera();
|
|
||||||
Matrix4f matrix = event.getPoseStack().last().pose();
|
|
||||||
BufferBuilder builder = Tesselator.getInstance().getBuilder();
|
|
||||||
RenderSystem.disableDepthTest();
|
|
||||||
RenderSystem.enableBlend();
|
|
||||||
RenderSystem.defaultBlendFunc();
|
|
||||||
RenderSystem.disableCull();
|
|
||||||
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
|
||||||
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
|
||||||
|
|
||||||
float size = (float) (CursorAimClientConfig.MARKER_SIZE.get() * MARKER_DISTANCE);
|
|
||||||
if (CursorAimClientConfig.SHOW_RETICLE.get()) {
|
|
||||||
plus(builder, matrix, camera, reticleWorld, size * 0.35F, RETICLE_COLOR);
|
|
||||||
}
|
|
||||||
ring(builder, matrix, camera, cursorWorld, size, CURSOR_COLOR);
|
|
||||||
BufferUploader.drawWithShader(builder.end());
|
|
||||||
|
|
||||||
RenderSystem.enableCull();
|
|
||||||
RenderSystem.enableDepthTest();
|
|
||||||
RenderSystem.disableBlend();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector3f screenPoint(Camera camera, Vector3d direction) {
|
|
||||||
Vector3f center = new Vector3f((float) direction.x, (float) direction.y, (float) direction.z);
|
|
||||||
if (center.lengthSquared() < 1.0E-9F) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return center.normalize().mul(MARKER_DISTANCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ring(BufferBuilder builder, Matrix4f matrix, Camera camera, Vector3d direction, float radius, int color) {
|
|
||||||
Vector3f center = screenPoint(camera, direction);
|
|
||||||
if (center == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Vector3f left = new Vector3f(camera.getLeftVector());
|
|
||||||
Vector3f up = new Vector3f(camera.getUpVector());
|
|
||||||
float thickness = radius * 0.12F;
|
|
||||||
GunPipperRenderer.ring(builder, matrix, center, left, up, radius, thickness, color);
|
|
||||||
GunPipperRenderer.quad(builder, matrix, center, left, up, thickness * 1.4F, thickness * 1.4F, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void plus(BufferBuilder builder, Matrix4f matrix, Camera camera, Vector3d direction, float arm, int color) {
|
|
||||||
Vector3f center = screenPoint(camera, direction);
|
|
||||||
if (center == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Vector3f left = new Vector3f(camera.getLeftVector());
|
|
||||||
Vector3f up = new Vector3f(camera.getUpVector());
|
|
||||||
float thickness = arm * 0.14F;
|
|
||||||
GunPipperRenderer.quad(builder, matrix, center, left, up, arm, thickness, color);
|
|
||||||
GunPipperRenderer.quad(builder, matrix, center, left, up, thickness, arm, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Vector helpers on arrays of three ----
|
|
||||||
|
|
||||||
/** Rotates v about the unit axis by an angle in radians (Rodrigues). */
|
|
||||||
private static double[] rotate(double[] v, double[] axis, double angle) {
|
|
||||||
double c = Math.cos(angle);
|
|
||||||
double s = Math.sin(angle);
|
|
||||||
double dot = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2];
|
|
||||||
double cx = axis[1] * v[2] - axis[2] * v[1];
|
|
||||||
double cy = axis[2] * v[0] - axis[0] * v[2];
|
|
||||||
double cz = axis[0] * v[1] - axis[1] * v[0];
|
|
||||||
return new double[]{
|
|
||||||
v[0] * c + cx * s + axis[0] * dot * (1.0 - c),
|
|
||||||
v[1] * c + cy * s + axis[1] * dot * (1.0 - c),
|
|
||||||
v[2] * c + cz * s + axis[2] * dot * (1.0 - c)};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Turns unit vector a towards unit vector b by a fraction of the angle between them. */
|
|
||||||
private static double[] turnTowards(double[] a, double[] b, double fraction) {
|
|
||||||
double dot = Mth.clamp(a[0] * b[0] + a[1] * b[1] + a[2] * b[2], -1.0, 1.0);
|
|
||||||
double cx = a[1] * b[2] - a[2] * b[1];
|
|
||||||
double cy = a[2] * b[0] - a[0] * b[2];
|
|
||||||
double cz = a[0] * b[1] - a[1] * b[0];
|
|
||||||
double sin = Math.sqrt(cx * cx + cy * cy + cz * cz);
|
|
||||||
if (sin < 1.0E-9) {
|
|
||||||
return dot > 0.0 ? b.clone() : a.clone();
|
|
||||||
}
|
|
||||||
double[] axis = {cx / sin, cy / sin, cz / sin};
|
|
||||||
return rotate(a, axis, Math.acos(dot) * fraction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.aim;
|
|
||||||
|
|
||||||
import net.minecraftforge.common.ForgeConfigSpec;
|
|
||||||
|
|
||||||
/** Client side settings of cursor aim. Lives in improved_aerodynamics-cursor-aim-client.toml. */
|
|
||||||
public final class CursorAimClientConfig {
|
|
||||||
public static final ForgeConfigSpec SPEC;
|
|
||||||
|
|
||||||
public static final ForgeConfigSpec.DoubleValue LOOK_AHEAD;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MOUSE_SENSITIVITY;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue SPRING_BACK_SECONDS;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_CURSOR_ANGLE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue ZOOM_STEP;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue ZOOM_MAX;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MARKER_SIZE;
|
|
||||||
public static final ForgeConfigSpec.BooleanValue SHOW_RETICLE;
|
|
||||||
|
|
||||||
static {
|
|
||||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
|
||||||
builder.comment("Cursor aim, client side").push("cursor_aim");
|
|
||||||
LOOK_AHEAD = builder.comment("How strongly the camera looks into the direction of the turn. 0 never moves it, 1 (100%) turns it quickly",
|
|
||||||
"to look where the cursor points. In between it follows more slowly")
|
|
||||||
.defineInRange("lookAhead", 0.0, 0.0, 1.0);
|
|
||||||
MOUSE_SENSITIVITY = builder.comment("Multiplier for how far the mouse moves the cursor (or the camera while the free look key is held)")
|
|
||||||
.defineInRange("mouseSensitivity", 1.0, 0.05, 10.0);
|
|
||||||
SPRING_BACK_SECONDS = builder.comment("The cursor returns to the gun reticle with this time constant in seconds when you stop moving the mouse.",
|
|
||||||
"0 makes it stay where you left it")
|
|
||||||
.defineInRange("springBackSeconds", 1.0, 0.0, 10.0);
|
|
||||||
MAX_CURSOR_ANGLE_DEGREES = builder.comment("Furthest the cursor can go from the gun reticle, 135 is a 270 degree cone")
|
|
||||||
.defineInRange("maxCursorAngleDegrees", 135.0, 10.0, 180.0);
|
|
||||||
ZOOM_STEP = builder.comment("Blocks the camera moves per mouse wheel notch")
|
|
||||||
.defineInRange("zoomStep", 2.0, 0.1, 20.0);
|
|
||||||
ZOOM_MAX = builder.comment("Furthest the camera can be pulled back from its normal distance, in blocks")
|
|
||||||
.defineInRange("zoomMax", 80.0, 0.0, 500.0);
|
|
||||||
MARKER_SIZE = builder.comment("Radius of the cursor ring on screen")
|
|
||||||
.defineInRange("markerSize", 0.04, 0.01, 0.3);
|
|
||||||
SHOW_RETICLE = builder.comment("Draw a small cross where the gun reticle points, the zero of the cursor, so you can see it when the camera looks away")
|
|
||||||
.define("showReticle", true);
|
|
||||||
builder.pop();
|
|
||||||
SPEC = builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private CursorAimClientConfig() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flight of a Warium shell, which is a vanilla arrow: every tick it moves by its velocity, the velocity is then scaled
|
|
||||||
* by the drag and gravity is subtracted. That is linear in the launch velocity, so the position after n ticks is
|
|
||||||
* <pre>M + D(n) * v0 - (0, G(n), 0)</pre>
|
|
||||||
* with D(n) the sum of the drag factors and G(n) the drop gravity causes on its own. Both are tabulated once, which
|
|
||||||
* makes both the impact point and the lead solution cheap enough to run every frame.
|
|
||||||
* <p>
|
|
||||||
* Plain doubles and no Minecraft classes, so it can be tested on its own. Units are blocks and ticks.
|
|
||||||
*/
|
|
||||||
public final class Ballistics {
|
|
||||||
public static final int MAX_TICKS = 400;
|
|
||||||
|
|
||||||
private final double[] d = new double[MAX_TICKS + 1];
|
|
||||||
private final double[] g = new double[MAX_TICKS + 1];
|
|
||||||
|
|
||||||
public Ballistics(double drag, double gravity) {
|
|
||||||
double y = 0.0;
|
|
||||||
double vy = 0.0;
|
|
||||||
double sum = 0.0;
|
|
||||||
double factor = 1.0;
|
|
||||||
for (int n = 1; n <= MAX_TICKS; n++) {
|
|
||||||
y += vy;
|
|
||||||
vy = vy * drag - gravity;
|
|
||||||
sum += factor;
|
|
||||||
factor *= drag;
|
|
||||||
d[n] = sum;
|
|
||||||
g[n] = -y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sum of the drag factors after n ticks, n may be fractional. */
|
|
||||||
public double d(double n) {
|
|
||||||
return interpolate(d, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Drop in blocks after n ticks, n may be fractional. */
|
|
||||||
public double g(double n) {
|
|
||||||
return interpolate(g, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double interpolate(double[] table, double n) {
|
|
||||||
if (n <= 0.0) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
if (n >= MAX_TICKS) {
|
|
||||||
return table[MAX_TICKS];
|
|
||||||
}
|
|
||||||
int i = (int) n;
|
|
||||||
double f = n - i;
|
|
||||||
return table[i] + (table[i + 1] - table[i]) * f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Height of the terrain a shell would hit at a column. */
|
|
||||||
public interface Ground {
|
|
||||||
double heightAt(double x, double z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Where a shell fired from the muzzle with the given world velocity (blocks per tick, already including the
|
|
||||||
* speed of the shooter if shells inherit it) comes down. Returns the impact point as {x, y, z} or null if it
|
|
||||||
* stays in the air for the whole table.
|
|
||||||
*/
|
|
||||||
public double[] impact(double mx, double my, double mz, double vx, double vy, double vz, Ground ground) {
|
|
||||||
double px = mx;
|
|
||||||
double py = my;
|
|
||||||
double pz = mz;
|
|
||||||
for (int n = 1; n <= MAX_TICKS; n++) {
|
|
||||||
double nx = mx + d[n] * vx;
|
|
||||||
double ny = my + d[n] * vy - g[n];
|
|
||||||
double nz = mz + d[n] * vz;
|
|
||||||
double floor = ground.heightAt(nx, nz);
|
|
||||||
if (ny <= floor) {
|
|
||||||
// Linear crossing inside this step
|
|
||||||
double before = py - ground.heightAt(px, pz);
|
|
||||||
double after = ny - floor;
|
|
||||||
double f = before - after > 1.0E-9 ? Math.min(1.0, Math.max(0.0, before / (before - after))) : 1.0;
|
|
||||||
return new double[] {px + (nx - px) * f, py + (ny - py) * f, pz + (nz - pz) * f};
|
|
||||||
}
|
|
||||||
px = nx;
|
|
||||||
py = ny;
|
|
||||||
pz = nz;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Result of {@link #lead}: aim direction, time of flight in ticks and the range to the target's current position. */
|
|
||||||
public record Lead(double dirX, double dirY, double dirZ, double ticks, double range) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aim direction that makes a shell meet a moving target.
|
|
||||||
*
|
|
||||||
* @param speed muzzle speed in blocks per tick
|
|
||||||
* @param t* target position now
|
|
||||||
* @param tv* target velocity in blocks per tick
|
|
||||||
* @param sv* velocity the shell inherits from the shooter in blocks per tick, zero if it does not
|
|
||||||
* @return the solution or null if the shell cannot reach the target within the table
|
|
||||||
*/
|
|
||||||
public Lead lead(double mx, double my, double mz, double speed, double tx, double ty, double tz,
|
|
||||||
double tvx, double tvy, double tvz, double svx, double svy, double svz) {
|
|
||||||
double range = Math.sqrt(sq(tx - mx) + sq(ty - my) + sq(tz - mz));
|
|
||||||
double previousN = 0.0;
|
|
||||||
for (int n = 1; n <= MAX_TICKS; n++) {
|
|
||||||
double f = miss(n, mx, my, mz, speed, tx, ty, tz, tvx, tvy, tvz, svx, svy, svz);
|
|
||||||
if (f <= 0.0) {
|
|
||||||
double lo = previousN;
|
|
||||||
double hi = n;
|
|
||||||
for (int i = 0; i < 30; i++) {
|
|
||||||
double mid = 0.5 * (lo + hi);
|
|
||||||
if (miss(mid, mx, my, mz, speed, tx, ty, tz, tvx, tvy, tvz, svx, svy, svz) > 0.0) {
|
|
||||||
lo = mid;
|
|
||||||
} else {
|
|
||||||
hi = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double t = 0.5 * (lo + hi);
|
|
||||||
double dn = d(t);
|
|
||||||
double rx = tx + tvx * t - mx - dn * svx;
|
|
||||||
double ry = ty + tvy * t - my - dn * svy + g(t);
|
|
||||||
double rz = tz + tvz * t - mz - dn * svz;
|
|
||||||
double length = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
||||||
if (length < 1.0E-6) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Lead(rx / length, ry / length, rz / length, t, range);
|
|
||||||
}
|
|
||||||
previousN = n;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Distance the shell can still cover short of where the target will be, positive means it has not got there yet. */
|
|
||||||
private double miss(double n, double mx, double my, double mz, double speed, double tx, double ty, double tz,
|
|
||||||
double tvx, double tvy, double tvz, double svx, double svy, double svz) {
|
|
||||||
double dn = d(n);
|
|
||||||
double rx = tx + tvx * n - mx - dn * svx;
|
|
||||||
double ry = ty + tvy * n - my - dn * svy + g(n);
|
|
||||||
double rz = tz + tvz * n - mz - dn * svz;
|
|
||||||
return Math.sqrt(rx * rx + ry * ry + rz * rz) - speed * dn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double sq(double v) {
|
|
||||||
return v * v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
|
||||||
|
|
||||||
import net.minecraft.client.multiplayer.ClientLevel;
|
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.core.Direction;
|
|
||||||
import net.minecraft.core.registries.Registries;
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
|
||||||
import net.minecraft.tags.TagKey;
|
|
||||||
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.level.chunk.ChunkAccess;
|
|
||||||
import net.minecraft.world.level.chunk.ChunkStatus;
|
|
||||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
|
||||||
import net.minecraftforge.fml.ModList;
|
|
||||||
import net.minecraftforge.registries.ForgeRegistries;
|
|
||||||
import org.joml.primitives.AABBic;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds the light autocannon of a ship and works out what it will shoot, the same way Warium's LACFireScript does:
|
|
||||||
* barrel count, muzzle, aim direction and muzzle speed.
|
|
||||||
*/
|
|
||||||
final class GunFinder {
|
|
||||||
private static final ResourceLocation LIGHT_AUTOCANNON = new ResourceLocation("crusty_chunks", "light_autocannon");
|
|
||||||
private static final TagKey<Block> BARREL = TagKey.create(Registries.BLOCK, new ResourceLocation("crusty_chunks", "acbarrel"));
|
|
||||||
private static final TagKey<Block> PASSTHROUGH = TagKey.create(Registries.BLOCK, new ResourceLocation("crusty_chunks", "barrelpassthrough"));
|
|
||||||
private static final int RESCAN_TICKS = 100;
|
|
||||||
private static final int MAX_GUNS = 16;
|
|
||||||
|
|
||||||
/** Muzzle and direction in ship space and the speed of the shell in blocks per tick. */
|
|
||||||
record Shot(double mx, double my, double mz, double dx, double dy, double dz, double speed) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private long shipId = Long.MIN_VALUE;
|
|
||||||
private long lastScan = Long.MIN_VALUE;
|
|
||||||
private final List<BlockPos> guns = new ArrayList<>();
|
|
||||||
|
|
||||||
/** The gun closest to the seat, or null if the ship has none loaded. */
|
|
||||||
Shot find(ClientLevel level, long shipId, AABBic aabb, BlockPos seat) {
|
|
||||||
if (shipId != this.shipId || level.getGameTime() - lastScan >= RESCAN_TICKS || level.getGameTime() < lastScan) {
|
|
||||||
this.shipId = shipId;
|
|
||||||
lastScan = level.getGameTime();
|
|
||||||
scan(level, aabb);
|
|
||||||
}
|
|
||||||
BlockPos best = null;
|
|
||||||
double bestDistance = Double.MAX_VALUE;
|
|
||||||
for (BlockPos pos : guns) {
|
|
||||||
double distance = pos.distSqr(seat);
|
|
||||||
if (distance < bestDistance) {
|
|
||||||
bestDistance = distance;
|
|
||||||
best = pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best == null ? null : shotOf(level, best);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scan(ClientLevel level, AABBic aabb) {
|
|
||||||
guns.clear();
|
|
||||||
Block gun = ForgeRegistries.BLOCKS.getValue(LIGHT_AUTOCANNON);
|
|
||||||
if (gun == null || aabb == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
|
||||||
for (int cx = aabb.minX() >> 4; cx <= aabb.maxX() >> 4; cx++) {
|
|
||||||
for (int cz = aabb.minZ() >> 4; cz <= aabb.maxZ() >> 4; cz++) {
|
|
||||||
ChunkAccess chunk = level.getChunk(cx, cz, ChunkStatus.FULL, false);
|
|
||||||
if (chunk == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
LevelChunkSection[] sections = chunk.getSections();
|
|
||||||
for (int i = 0; i < sections.length; i++) {
|
|
||||||
LevelChunkSection section = sections[i];
|
|
||||||
if (section.hasOnlyAir() || !section.maybeHas(state -> state.is(gun))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int baseY = level.getSectionYFromSectionIndex(i) << 4;
|
|
||||||
for (int x = 0; x < 16; x++) {
|
|
||||||
for (int y = 0; y < 16; y++) {
|
|
||||||
for (int z = 0; z < 16; z++) {
|
|
||||||
if (section.getBlockState(x, y, z).is(gun) && guns.size() < MAX_GUNS) {
|
|
||||||
guns.add(pos.set((cx << 4) + x, baseY + y, (cz << 4) + z).immutable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Shot shotOf(ClientLevel level, BlockPos gun) {
|
|
||||||
BlockState state = level.getBlockState(gun);
|
|
||||||
Property<?> property = state.getBlock().getStateDefinition().getProperty("facing");
|
|
||||||
if (!(property instanceof DirectionProperty facingProperty)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Direction facing = state.getValue(facingProperty);
|
|
||||||
int fx = facing.getStepX();
|
|
||||||
int fy = facing.getStepY();
|
|
||||||
int fz = facing.getStepZ();
|
|
||||||
|
|
||||||
// Barrels, counted like Warium does: up to five more behind the first one
|
|
||||||
double barrels = 1.0;
|
|
||||||
for (int i = 0; i < 5 && level.getBlockState(gun.offset(fx * (int) (1 + barrels), fy * (int) (1 + barrels), fz * (int) (1 + barrels))).is(BARREL); i++) {
|
|
||||||
barrels++;
|
|
||||||
}
|
|
||||||
// Muzzle: past the barrels, skipping through blocks the shell passes
|
|
||||||
double position = barrels;
|
|
||||||
for (int i = 0; i < 10; i++) {
|
|
||||||
BlockState at = level.getBlockState(gun.offset(fx * (int) (1 + position), fy * (int) (1 + position), fz * (int) (1 + position)));
|
|
||||||
if ((!at.isAir() && !at.canOcclude()) || at.is(PASSTHROUGH)) {
|
|
||||||
position++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double mx = gun.getX() + fx * (1.5 + position) + 0.5;
|
|
||||||
double my = gun.getY() + fy * (1.5 + position) + 0.5;
|
|
||||||
double mz = gun.getZ() + fz * (1.5 + position) + 0.5;
|
|
||||||
|
|
||||||
// Aim, from the tags of the gun itself
|
|
||||||
double aimX = 0.0;
|
|
||||||
double aimY = 0.0;
|
|
||||||
double aimZ = 0.0;
|
|
||||||
BlockEntity entity = level.getBlockEntity(gun);
|
|
||||||
double gx = entity == null ? 0.0 : entity.getPersistentData().getDouble("X");
|
|
||||||
double gz = entity == null ? 0.0 : entity.getPersistentData().getDouble("Z");
|
|
||||||
double gpitch = entity == null ? 0.0 : entity.getPersistentData().getDouble("Pitch");
|
|
||||||
if (1.0 > Math.abs(fx - gx) || 1.0 > Math.abs(fz - gz)) {
|
|
||||||
aimY = gpitch;
|
|
||||||
aimX = gx + fx;
|
|
||||||
aimZ = gz + fz;
|
|
||||||
}
|
|
||||||
if (aimX * aimZ == 0.0) {
|
|
||||||
aimX = fx;
|
|
||||||
aimZ = fz;
|
|
||||||
}
|
|
||||||
double length = Math.sqrt(aimX * aimX + aimY * aimY + aimZ * aimZ);
|
|
||||||
if (length < 1.0E-6) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
double speed = PipperConfig.MUZZLE_SPEED_OVERRIDE.get();
|
|
||||||
if (speed <= 0.0) {
|
|
||||||
double multiplier = ModList.get().isLoaded("ritchiesprojectilelib") ? 2.0 : 1.0;
|
|
||||||
speed = (5.0 + barrels / 1.5) * multiplier;
|
|
||||||
}
|
|
||||||
return new Shot(mx, my, mz, aimX / length, aimY / length, aimZ / length, speed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
|
||||||
|
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
|
||||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
|
||||||
import com.mojang.blaze3d.vertex.BufferUploader;
|
|
||||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
|
||||||
import com.mojang.blaze3d.vertex.Tesselator;
|
|
||||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
|
||||||
import dev.lumareth.improvedAerodynamics.ImprovedAerodynamics;
|
|
||||||
import net.minecraft.client.Camera;
|
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.client.multiplayer.ClientLevel;
|
|
||||||
import net.minecraft.client.renderer.GameRenderer;
|
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.util.Mth;
|
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
|
||||||
import net.minecraft.world.phys.Vec3;
|
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
|
||||||
import net.minecraftforge.client.event.RenderLevelStageEvent;
|
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
|
||||||
import net.minecraftforge.fml.ModList;
|
|
||||||
import net.minecraftforge.fml.common.Mod;
|
|
||||||
import org.joml.Matrix4f;
|
|
||||||
import org.joml.Vector3d;
|
|
||||||
import org.joml.Vector3f;
|
|
||||||
import org.valkyrienskies.core.api.ships.ClientShip;
|
|
||||||
import org.valkyrienskies.core.api.ships.Ship;
|
|
||||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws two markers for the first light autocannon of the aircraft you sit in:
|
|
||||||
* an orange ring where its shells come down on the ground, and, for the radar target selected on the seat, a green
|
|
||||||
* ring you put on the target to hit it (the lead pipper): green from the radar, cyan while following the ship itself,
|
|
||||||
* yellow while only extrapolating a lost contact. Both come from the same ballistics Warium's shells follow.
|
|
||||||
*/
|
|
||||||
@Mod.EventBusSubscriber(modid = ImprovedAerodynamics.MOD_ID, value = Dist.CLIENT)
|
|
||||||
public final class GunPipperRenderer {
|
|
||||||
private static final int IMPACT_COLOR = 0xFFFF9020;
|
|
||||||
private static final int LEAD_COLOR = 0xFF40FF60;
|
|
||||||
private static final int VISUAL_COLOR = 0xFF40E8FF;
|
|
||||||
private static final int COAST_COLOR = 0xFFFFE040;
|
|
||||||
private static final float DISTANCE = 2.0F;
|
|
||||||
|
|
||||||
private static final GunFinder FINDER = new GunFinder();
|
|
||||||
private static final TargetTrack TRACK = new TargetTrack();
|
|
||||||
private static Ballistics ballistics;
|
|
||||||
private static double ballisticsDrag = Double.NaN;
|
|
||||||
private static double ballisticsGravity = Double.NaN;
|
|
||||||
|
|
||||||
private GunPipperRenderer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onRenderLevel(RenderLevelStageEvent event) {
|
|
||||||
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES || !PipperConfig.SPEC.isLoaded()
|
|
||||||
|| !PipperConfig.ENABLED.get() || !ModList.get().isLoaded("valkyrien_warium")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
|
||||||
ClientLevel level = minecraft.level;
|
|
||||||
if (level == null || minecraft.player == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Entity seat = WariumSeat.seatOf(minecraft.player);
|
|
||||||
if (seat == null) {
|
|
||||||
TRACK.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BlockPos seatPos = BlockPos.containing(seat.getX(), seat.getY(), seat.getZ());
|
|
||||||
Ship ship = VSGameUtilsKt.getLoadedShipManagingPos(level, seatPos);
|
|
||||||
if (ship == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GunFinder.Shot shot = FINDER.find(level, ship.getId(), ship.getShipAABB(), seatPos);
|
|
||||||
if (shot == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
org.joml.Matrix4dc shipToWorld = ship instanceof ClientShip clientShip
|
|
||||||
? clientShip.getRenderTransform().getShipToWorld() : ship.getShipToWorld();
|
|
||||||
Vector3d muzzle = shipToWorld.transformPosition(new Vector3d(shot.mx(), shot.my(), shot.mz()));
|
|
||||||
Vector3d direction = shipToWorld.transformDirection(new Vector3d(shot.dx(), shot.dy(), shot.dz())).normalize();
|
|
||||||
|
|
||||||
double inherit = PipperConfig.INHERIT_SHIP_VELOCITY.get() ? 1.0 / 20.0 : 0.0;
|
|
||||||
org.joml.Vector3dc velocity = ship.getVelocity();
|
|
||||||
double svx = velocity.x() * inherit;
|
|
||||||
double svy = velocity.y() * inherit;
|
|
||||||
double svz = velocity.z() * inherit;
|
|
||||||
|
|
||||||
Ballistics table = table();
|
|
||||||
Camera camera = event.getCamera();
|
|
||||||
Matrix4f matrix = event.getPoseStack().last().pose();
|
|
||||||
|
|
||||||
BufferBuilder builder = Tesselator.getInstance().getBuilder();
|
|
||||||
RenderSystem.disableDepthTest();
|
|
||||||
RenderSystem.enableBlend();
|
|
||||||
RenderSystem.defaultBlendFunc();
|
|
||||||
RenderSystem.disableCull();
|
|
||||||
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
|
||||||
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
|
|
||||||
boolean drawn = false;
|
|
||||||
|
|
||||||
if (PipperConfig.SHOW_IMPACT.get()) {
|
|
||||||
double[] impact = table.impact(muzzle.x, muzzle.y, muzzle.z,
|
|
||||||
direction.x * shot.speed() + svx, direction.y * shot.speed() + svy, direction.z * shot.speed() + svz,
|
|
||||||
(x, z) -> level.getHeight(Heightmap.Types.MOTION_BLOCKING, Mth.floor(x), Mth.floor(z)));
|
|
||||||
if (impact != null) {
|
|
||||||
drawn |= marker(builder, matrix, camera, impact[0], impact[1], impact[2], IMPACT_COLOR, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PipperConfig.SHOW_LEAD.get()) {
|
|
||||||
drawn |= lead(builder, matrix, camera, level, seat, seatPos, ship.getId(), table, muzzle, shot.speed(), svx, svy, svz);
|
|
||||||
} else {
|
|
||||||
TRACK.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawn) {
|
|
||||||
BufferUploader.drawWithShader(builder.end());
|
|
||||||
} else {
|
|
||||||
builder.end().release();
|
|
||||||
}
|
|
||||||
RenderSystem.enableCull();
|
|
||||||
RenderSystem.enableDepthTest();
|
|
||||||
RenderSystem.disableBlend();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean lead(BufferBuilder builder, Matrix4f matrix, Camera camera, ClientLevel level, Entity seat, BlockPos seatPos,
|
|
||||||
long ownShipId, Ballistics table, Vector3d muzzle, double speed, double svx, double svy, double svz) {
|
|
||||||
int selected = WariumSeat.selectedTarget(seat);
|
|
||||||
double[] radar = selected > 0 ? radarPosition(level, seatPos, selected) : null;
|
|
||||||
TargetTrack.Fix fix = TRACK.resolve(level, ownShipId, selected, radar, level.getGameTime(),
|
|
||||||
PipperConfig.VISUAL_TRACK.get(), PipperConfig.COAST_SECONDS.get());
|
|
||||||
if (fix == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Ballistics.Lead lead = table.lead(muzzle.x, muzzle.y, muzzle.z, speed, fix.x(), fix.y(), fix.z(),
|
|
||||||
fix.vx(), fix.vy(), fix.vz(), svx, svy, svz);
|
|
||||||
if (lead == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int color = switch (fix.source()) {
|
|
||||||
case RADAR -> LEAD_COLOR;
|
|
||||||
case VISUAL -> VISUAL_COLOR;
|
|
||||||
case COAST -> COAST_COLOR;
|
|
||||||
};
|
|
||||||
return marker(builder, matrix, camera,
|
|
||||||
muzzle.x + lead.dirX() * lead.range(), muzzle.y + lead.dirY() * lead.range(), muzzle.z + lead.dirZ() * lead.range(),
|
|
||||||
color, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Position the node reports for a target index, or null if it reports none. */
|
|
||||||
private static double[] radarPosition(ClientLevel level, BlockPos seatPos, int selected) {
|
|
||||||
BlockEntity seatEntity = level.getBlockEntity(seatPos);
|
|
||||||
if (seatEntity == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var seatData = seatEntity.getPersistentData();
|
|
||||||
BlockEntity node = level.getBlockEntity(BlockPos.containing(seatData.getDouble("ControlX"), seatData.getDouble("ControlY"), seatData.getDouble("ControlZ")));
|
|
||||||
if (node == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var data = node.getPersistentData();
|
|
||||||
// Warium builds these keys from a double, so they read TargetX1.0, TargetY1.0 and so on
|
|
||||||
double tx = data.getDouble("TargetX" + (double) selected);
|
|
||||||
double ty = data.getDouble("TargetY" + (double) selected);
|
|
||||||
double tz = data.getDouble("TargetZ" + (double) selected);
|
|
||||||
if (tx == 0.0 && ty == 0.0 && tz == 0.0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new double[] {tx, ty, tz};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Ballistics table() {
|
|
||||||
double drag = PipperConfig.DRAG.get();
|
|
||||||
double gravity = PipperConfig.GRAVITY.get();
|
|
||||||
if (ballistics == null || drag != ballisticsDrag || gravity != ballisticsGravity) {
|
|
||||||
ballistics = new Ballistics(drag, gravity);
|
|
||||||
ballisticsDrag = drag;
|
|
||||||
ballisticsGravity = gravity;
|
|
||||||
}
|
|
||||||
return ballistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A ring on the screen in the direction of a world point, with ticks on it for the lead pipper. */
|
|
||||||
private static boolean marker(BufferBuilder builder, Matrix4f matrix, Camera camera, double x, double y, double z, int color, boolean ticks) {
|
|
||||||
Vec3 cameraPosition = camera.getPosition();
|
|
||||||
Vector3f toPoint = new Vector3f((float) (x - cameraPosition.x), (float) (y - cameraPosition.y), (float) (z - cameraPosition.z));
|
|
||||||
if (toPoint.lengthSquared() < 1.0E-6F) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
toPoint.normalize();
|
|
||||||
if (toPoint.dot(camera.getLookVector()) <= 0.05F) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector3f center = new Vector3f(toPoint).mul(DISTANCE);
|
|
||||||
Vector3f left = new Vector3f(camera.getLeftVector());
|
|
||||||
Vector3f up = new Vector3f(camera.getUpVector());
|
|
||||||
float radius = (float) (PipperConfig.MARKER_SIZE.get() * DISTANCE);
|
|
||||||
float thickness = radius * 0.12F;
|
|
||||||
|
|
||||||
ring(builder, matrix, center, left, up, radius, thickness, color);
|
|
||||||
quad(builder, matrix, center, left, up, thickness * 1.6F, thickness * 1.6F, color);
|
|
||||||
if (ticks) {
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
double angle = i * Math.PI / 2.0;
|
|
||||||
float cx = (float) Math.cos(angle);
|
|
||||||
float cy = (float) Math.sin(angle);
|
|
||||||
Vector3f tickCenter = new Vector3f(center)
|
|
||||||
.fma(cx * radius * 1.45F, left).fma(cy * radius * 1.45F, up);
|
|
||||||
float halfX = Math.abs(cx) > 0.5F ? radius * 0.35F : thickness;
|
|
||||||
float halfY = Math.abs(cy) > 0.5F ? radius * 0.35F : thickness;
|
|
||||||
quad(builder, matrix, tickCenter, left, up, halfX, halfY, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ring(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float radius, float thickness, int color) {
|
|
||||||
final int segments = 32;
|
|
||||||
float inner = radius - thickness;
|
|
||||||
float outer = radius + thickness;
|
|
||||||
for (int i = 0; i < segments; i++) {
|
|
||||||
double a0 = i * 2.0 * Math.PI / segments;
|
|
||||||
double a1 = (i + 1) * 2.0 * Math.PI / segments;
|
|
||||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a0) * inner, (float) Math.sin(a0) * inner, color);
|
|
||||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a0) * outer, (float) Math.sin(a0) * outer, color);
|
|
||||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a1) * outer, (float) Math.sin(a1) * outer, color);
|
|
||||||
vertex(builder, matrix, center, left, up, (float) Math.cos(a1) * inner, (float) Math.sin(a1) * inner, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void quad(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float halfX, float halfY, int color) {
|
|
||||||
vertex(builder, matrix, center, left, up, -halfX, -halfY, color);
|
|
||||||
vertex(builder, matrix, center, left, up, halfX, -halfY, color);
|
|
||||||
vertex(builder, matrix, center, left, up, halfX, halfY, color);
|
|
||||||
vertex(builder, matrix, center, left, up, -halfX, halfY, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void vertex(BufferBuilder builder, Matrix4f matrix, Vector3f center, Vector3f left, Vector3f up, float a, float b, int color) {
|
|
||||||
// The left vector points to the left of the screen, so a positive offset along it is drawn to the left
|
|
||||||
float vx = center.x - left.x * a + up.x * b;
|
|
||||||
float vy = center.y - left.y * a + up.y * b;
|
|
||||||
float vz = center.z - left.z * a + up.z * b;
|
|
||||||
builder.vertex(matrix, vx, vy, vz)
|
|
||||||
.color(color >> 16 & 255, color >> 8 & 255, color & 255, color >>> 24)
|
|
||||||
.endVertex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
|
||||||
|
|
||||||
import net.minecraftforge.common.ForgeConfigSpec;
|
|
||||||
|
|
||||||
/** Client side settings of the gun pipper. Lives in improved_aerodynamics-client.toml. */
|
|
||||||
public final class PipperConfig {
|
|
||||||
public static final ForgeConfigSpec SPEC;
|
|
||||||
|
|
||||||
public static final ForgeConfigSpec.BooleanValue ENABLED;
|
|
||||||
public static final ForgeConfigSpec.BooleanValue SHOW_IMPACT;
|
|
||||||
public static final ForgeConfigSpec.BooleanValue SHOW_LEAD;
|
|
||||||
public static final ForgeConfigSpec.BooleanValue INHERIT_SHIP_VELOCITY;
|
|
||||||
public static final ForgeConfigSpec.BooleanValue VISUAL_TRACK;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue COAST_SECONDS;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MUZZLE_SPEED_OVERRIDE;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue GRAVITY;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue DRAG;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MARKER_SIZE;
|
|
||||||
|
|
||||||
static {
|
|
||||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
|
||||||
builder.comment("Gun pipper for Warium light autocannons, drawn while seated in a Warium seat").push("pipper");
|
|
||||||
ENABLED = builder.comment("Master switch")
|
|
||||||
.define("enabled", true);
|
|
||||||
SHOW_IMPACT = builder.comment("Show where the shells of the gun come down on the ground (orange ring)")
|
|
||||||
.define("showImpact", true);
|
|
||||||
SHOW_LEAD = builder.comment("Show the lead pipper for the radar target you selected (green ring)")
|
|
||||||
.define("showLead", true);
|
|
||||||
VISUAL_TRACK = builder.comment("Follow the selected target's ship itself once the radar has shown it, so the lead pipper keeps working when",
|
|
||||||
"the radar loses it (notching, speed gate, ground clutter). Only while the ship is loaded, which is within render distance",
|
|
||||||
"(cyan ring)")
|
|
||||||
.define("visualTrack", true);
|
|
||||||
COAST_SECONDS = builder.comment("Seconds a lost radar contact is extrapolated from its last position and velocity when its ship is not loaded",
|
|
||||||
"(yellow ring), 0 turns it off")
|
|
||||||
.defineInRange("coastSeconds", 8.0, 0.0, 60.0);
|
|
||||||
INHERIT_SHIP_VELOCITY = builder.comment("Add the speed of your aircraft to the shells. Warium does not do this itself, only turn it",
|
|
||||||
"on if a test shows your shells keep the speed of the jet (fire level at full speed, then hovering)")
|
|
||||||
.define("inheritShipVelocity", false);
|
|
||||||
MUZZLE_SPEED_OVERRIDE = builder.comment("Muzzle speed in blocks per tick, 0 works it out from the barrel length like Warium does")
|
|
||||||
.defineInRange("muzzleSpeedOverride", 0.0, 0.0, 100.0);
|
|
||||||
GRAVITY = builder.comment("Gravity of the shells in blocks per tick squared")
|
|
||||||
.defineInRange("gravity", 0.05, 0.0, 1.0);
|
|
||||||
DRAG = builder.comment("Speed the shells keep each tick, 0.99 is a vanilla arrow")
|
|
||||||
.defineInRange("drag", 0.99, 0.5, 1.0);
|
|
||||||
MARKER_SIZE = builder.comment("Radius of the markers on screen")
|
|
||||||
.defineInRange("markerSize", 0.03, 0.005, 0.2);
|
|
||||||
builder.pop();
|
|
||||||
SPEC = builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PipperConfig() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
|
||||||
|
|
||||||
import net.minecraft.client.multiplayer.ClientLevel;
|
|
||||||
import org.joml.Vector3d;
|
|
||||||
import org.joml.primitives.AABBdc;
|
|
||||||
import org.valkyrienskies.core.api.ships.ClientShip;
|
|
||||||
import org.valkyrienskies.mod.common.VSGameUtilsKt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The target the lead pipper aims at. The radar is only the way in: its contact is matched to the closest loaded ship
|
|
||||||
* and from then on that ship is followed directly, with its true position and velocity, so the pipper survives the
|
|
||||||
* radar losing the contact (notching, the speed gate, ground clutter). Without a loaded ship, which happens beyond
|
|
||||||
* render distance, the last radar position is extrapolated for a limited time.
|
|
||||||
* <p>
|
|
||||||
* The selected index is no help for this: the radar rebuilds its list, so after a loss the index can point at another
|
|
||||||
* contact or at an empty slot.
|
|
||||||
*/
|
|
||||||
final class TargetTrack {
|
|
||||||
/** A radar position this close to a ship is taken to be that ship. */
|
|
||||||
private static final double SNAP_BLOCKS = 80.0;
|
|
||||||
/** A fresh radar position this far from the followed ship means the player moved on to another target. */
|
|
||||||
private static final double REBIND_BLOCKS = 250.0;
|
|
||||||
private static final long NONE = Long.MIN_VALUE;
|
|
||||||
|
|
||||||
enum Source {
|
|
||||||
RADAR, VISUAL, COAST
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Position in blocks, velocity in blocks per tick. */
|
|
||||||
record Fix(double x, double y, double z, double vx, double vy, double vz, Source source) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private final TargetTracker radar = new TargetTracker();
|
|
||||||
private int index = -1;
|
|
||||||
private long boundId = NONE;
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
radar.reset();
|
|
||||||
index = -1;
|
|
||||||
boundId = NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param radarPosition the position the node reports for the selected index, or null if it reports none
|
|
||||||
*/
|
|
||||||
Fix resolve(ClientLevel level, long ownShipId, int selected, double[] radarPosition, long tick,
|
|
||||||
boolean visualTrack, double coastSeconds) {
|
|
||||||
if (selected <= 0) {
|
|
||||||
reset();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (selected != index) {
|
|
||||||
reset();
|
|
||||||
index = selected;
|
|
||||||
}
|
|
||||||
if (radarPosition != null) {
|
|
||||||
radar.update(selected, radarPosition[0], radarPosition[1], radarPosition[2], tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visualTrack) {
|
|
||||||
ClientShip bound = boundId == NONE ? null : shipById(level, boundId);
|
|
||||||
if (radarPosition != null && radar.age(tick) <= 1) {
|
|
||||||
// Fresh contact: pick the ship it belongs to, or let go of the old one if the player switched
|
|
||||||
if (bound == null || distance(center(bound), radarPosition) > REBIND_BLOCKS) {
|
|
||||||
bound = nearest(level, ownShipId, radarPosition);
|
|
||||||
boundId = bound == null ? NONE : bound.getId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bound != null) {
|
|
||||||
Vector3d center = center(bound);
|
|
||||||
var velocity = bound.getVelocity();
|
|
||||||
return new Fix(center.x, center.y, center.z, velocity.x() / 20.0, velocity.y() / 20.0, velocity.z() / 20.0, Source.VISUAL);
|
|
||||||
}
|
|
||||||
boundId = NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!radar.hasPosition()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
long age = radar.age(tick);
|
|
||||||
if (age > TargetTracker.STALE_TICKS) {
|
|
||||||
if (coastSeconds <= 0.0 || !radar.hasVelocity() || age > coastSeconds * 20.0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
double[] p = radar.predicted(tick);
|
|
||||||
return new Fix(p[0], p[1], p[2], radar.vx(), radar.vy(), radar.vz(), Source.COAST);
|
|
||||||
}
|
|
||||||
double[] p = radar.predicted(tick);
|
|
||||||
// A contact is only extrapolated up to the age of its last update
|
|
||||||
return new Fix(p[0], p[1], p[2], radar.vx(), radar.vy(), radar.vz(), Source.RADAR);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ClientShip shipById(ClientLevel level, long id) {
|
|
||||||
for (ClientShip ship : VSGameUtilsKt.getShipObjectWorld(level).getLoadedShips()) {
|
|
||||||
if (ship.getId() == id) {
|
|
||||||
return ship;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ClientShip nearest(ClientLevel level, long ownShipId, double[] position) {
|
|
||||||
ClientShip best = null;
|
|
||||||
double bestDistance = SNAP_BLOCKS;
|
|
||||||
for (ClientShip ship : VSGameUtilsKt.getShipObjectWorld(level).getLoadedShips()) {
|
|
||||||
if (ship.getId() == ownShipId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double distance = distance(center(ship), position);
|
|
||||||
if (distance < bestDistance) {
|
|
||||||
bestDistance = distance;
|
|
||||||
best = ship;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The same point Warium's radar reports: the center of the world bounding box. */
|
|
||||||
private static Vector3d center(ClientShip ship) {
|
|
||||||
AABBdc box = ship.getWorldAABB();
|
|
||||||
return box.center(new Vector3d());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double distance(Vector3d a, double[] b) {
|
|
||||||
return Math.sqrt(sq(a.x - b[0]) + sq(a.y - b[1]) + sq(a.z - b[2]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double sq(double v) {
|
|
||||||
return v * v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Target velocity from the radar. The node only stores positions, and they change when the radar updates, which is
|
|
||||||
* not every tick. So the velocity is the change between two distinct positions divided by the ticks between them,
|
|
||||||
* smoothed. When the radar stops updating the last velocity is kept so the track can be extrapolated.
|
|
||||||
*/
|
|
||||||
final class TargetTracker {
|
|
||||||
/** Ticks without a new position after which the contact counts as lost and is only extrapolated. */
|
|
||||||
static final long STALE_TICKS = 40;
|
|
||||||
private static final double SMOOTHING = 0.5;
|
|
||||||
private static final double JUMP_BLOCKS_PER_TICK = 15.0;
|
|
||||||
|
|
||||||
private int index = -1;
|
|
||||||
private double px;
|
|
||||||
private double py;
|
|
||||||
private double pz;
|
|
||||||
private long sampleTick;
|
|
||||||
private double vx;
|
|
||||||
private double vy;
|
|
||||||
private double vz;
|
|
||||||
private boolean hasVelocity;
|
|
||||||
|
|
||||||
void update(int selected, double x, double y, double z, long tick) {
|
|
||||||
if (selected != index || tick < sampleTick) {
|
|
||||||
index = selected;
|
|
||||||
px = x;
|
|
||||||
py = y;
|
|
||||||
pz = z;
|
|
||||||
sampleTick = tick;
|
|
||||||
hasVelocity = false;
|
|
||||||
vx = 0.0;
|
|
||||||
vy = 0.0;
|
|
||||||
vz = 0.0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (x != px || y != py || z != pz) {
|
|
||||||
long dt = Math.max(1L, tick - sampleTick);
|
|
||||||
double nx = (x - px) / dt;
|
|
||||||
double ny = (y - py) / dt;
|
|
||||||
double nz = (z - pz) / dt;
|
|
||||||
if (Math.sqrt(nx * nx + ny * ny + nz * nz) > JUMP_BLOCKS_PER_TICK) {
|
|
||||||
// Teleport or a different contact reusing the slot
|
|
||||||
hasVelocity = false;
|
|
||||||
} else if (hasVelocity) {
|
|
||||||
vx += (nx - vx) * SMOOTHING;
|
|
||||||
vy += (ny - vy) * SMOOTHING;
|
|
||||||
vz += (nz - vz) * SMOOTHING;
|
|
||||||
} else {
|
|
||||||
vx = nx;
|
|
||||||
vy = ny;
|
|
||||||
vz = nz;
|
|
||||||
hasVelocity = true;
|
|
||||||
}
|
|
||||||
px = x;
|
|
||||||
py = y;
|
|
||||||
pz = z;
|
|
||||||
sampleTick = tick;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
index = -1;
|
|
||||||
hasVelocity = false;
|
|
||||||
vx = 0.0;
|
|
||||||
vy = 0.0;
|
|
||||||
vz = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean hasPosition() {
|
|
||||||
return index >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ticks since the radar last gave a new position. */
|
|
||||||
long age(long tick) {
|
|
||||||
return tick - sampleTick;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The last radar position moved on by the velocity over the time since. */
|
|
||||||
double[] predicted(long tick) {
|
|
||||||
long dt = Math.max(0L, tick - sampleTick);
|
|
||||||
return new double[] {px + vx * dt, py + vy * dt, pz + vz * dt};
|
|
||||||
}
|
|
||||||
|
|
||||||
double lastX() {
|
|
||||||
return px;
|
|
||||||
}
|
|
||||||
|
|
||||||
double lastY() {
|
|
||||||
return py;
|
|
||||||
}
|
|
||||||
|
|
||||||
double lastZ() {
|
|
||||||
return pz;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Velocity in blocks per tick, zero when unknown. */
|
|
||||||
double vx() {
|
|
||||||
return vx;
|
|
||||||
}
|
|
||||||
|
|
||||||
double vy() {
|
|
||||||
return vy;
|
|
||||||
}
|
|
||||||
|
|
||||||
double vz() {
|
|
||||||
return vz;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean hasVelocity() {
|
|
||||||
return hasVelocity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.client.pipper;
|
|
||||||
|
|
||||||
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
|
|
||||||
/** Everything that touches Warium VS classes, so nothing else loads them when Warium is missing. */
|
|
||||||
final class WariumSeat {
|
|
||||||
private WariumSeat() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static Entity seatOf(Entity rider) {
|
|
||||||
Entity root = rider.getRootVehicle();
|
|
||||||
return root instanceof SeatEntityEntity ? root : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Index of the radar target selected on the seat, 1 based, 0 for none. */
|
|
||||||
static int selectedTarget(Entity seat) {
|
|
||||||
return seat.getEntityData().get(SeatEntityEntity.DATA_SelectedTarget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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
@@ -0,0 +1,113 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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
@@ -0,0 +1,24 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
private static final double INPUT_RATE_PER_TICK = 0.2;
|
private static final double INPUT_RATE_PER_TICK = 0.2;
|
||||||
private static final double VELOCITY_EPSILON = 0.05;
|
private static final double VELOCITY_EPSILON = 0.05;
|
||||||
private static final double ANGULAR_EPSILON = 0.005;
|
private static final double ANGULAR_EPSILON = 0.005;
|
||||||
private static final ControlAxis[] MEASURED_AXES = {ControlAxis.PITCH, ControlAxis.YAW, ControlAxis.ROLL};
|
|
||||||
|
|
||||||
private final SubstepClock clock;
|
private final SubstepClock clock;
|
||||||
|
|
||||||
@@ -58,22 +57,9 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
|
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
|
||||||
final Vector3d pitchResponse = new Vector3d();
|
final Vector3d pitchResponse = new Vector3d();
|
||||||
final Vector3d pitchResponseBuilding = new Vector3d();
|
final Vector3d pitchResponseBuilding = new Vector3d();
|
||||||
// The same for yaw and roll, measured while cursor aim flies the node
|
|
||||||
final Vector3d yawResponse = new Vector3d();
|
|
||||||
final Vector3d yawResponseBuilding = new Vector3d();
|
|
||||||
final Vector3d rollResponse = new Vector3d();
|
|
||||||
final Vector3d rollResponseBuilding = new Vector3d();
|
|
||||||
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
|
||||||
volatile double savedTrim;
|
volatile double savedTrim;
|
||||||
|
|
||||||
Vector3d responseBuilding(ControlAxis axis) {
|
|
||||||
return switch (axis) {
|
|
||||||
case YAW -> yawResponseBuilding;
|
|
||||||
case ROLL -> rollResponseBuilding;
|
|
||||||
default -> pitchResponseBuilding;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
NodeState(NodeData data) {
|
NodeState(NodeData data) {
|
||||||
pitch = data.pitch();
|
pitch = data.pitch();
|
||||||
yaw = data.yaw();
|
yaw = data.yaw();
|
||||||
@@ -225,8 +211,6 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
wingStalled = false;
|
wingStalled = false;
|
||||||
for (NodeState state : nodeStates.values()) {
|
for (NodeState state : nodeStates.values()) {
|
||||||
state.pitchResponseBuilding.zero();
|
state.pitchResponseBuilding.zero();
|
||||||
state.yawResponseBuilding.zero();
|
|
||||||
state.rollResponseBuilding.zero();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +221,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
r[2] = s.z() - centerOfMass.z();
|
r[2] = s.z() - centerOfMass.z();
|
||||||
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null;
|
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null;
|
||||||
NodeState state = node != null ? nodeStates.get(node.key()) : null;
|
NodeState state = node != null ? nodeStates.get(node.key()) : null;
|
||||||
double input = s.type().controllable ? input(s, node, state, ControlAxis.NONE, 0.0) : 0.0;
|
double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0;
|
||||||
|
|
||||||
if (improved) {
|
if (improved) {
|
||||||
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
|
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
|
||||||
@@ -249,62 +233,53 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
WariumAero.evaluate(s, v, w, r, input, weight, out);
|
WariumAero.evaluate(s, v, w, r, input, weight, out);
|
||||||
}
|
}
|
||||||
|
|
||||||
// How much a little more input on an axis would change the torque: pitch for auto trim, all three for
|
// How much a little more pitch input on this node would change the torque, for auto trim
|
||||||
// cursor aim
|
if (commit && node != null && node.autoTrim() && s.type().controllable && s.listensToPitch()) {
|
||||||
if (commit && node != null && s.type().controllable) {
|
double delta = 0.05;
|
||||||
boolean aimed = CursorAimRegistry.isActive(node.key());
|
probe.clear();
|
||||||
for (ControlAxis axis : MEASURED_AXES) {
|
double base = 0.0;
|
||||||
if (!s.listensTo(axis) || !(aimed || (axis == ControlAxis.PITCH && node.autoTrim()))) {
|
if (improved) {
|
||||||
continue;
|
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
|
||||||
}
|
} else {
|
||||||
double delta = 0.05;
|
WariumAero.evaluate(s, v, w, r, input, weight, probe);
|
||||||
probe.clear();
|
|
||||||
if (improved) {
|
|
||||||
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
|
|
||||||
} else {
|
|
||||||
WariumAero.evaluate(s, v, w, r, input, weight, probe);
|
|
||||||
}
|
|
||||||
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
|
|
||||||
probe.clear();
|
|
||||||
double nudged = input(s, node, state, axis, delta);
|
|
||||||
if (improved) {
|
|
||||||
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
|
|
||||||
} else {
|
|
||||||
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
|
||||||
}
|
|
||||||
state.responseBuilding(axis).add(
|
|
||||||
(probe.totalTorque(0) - before[0]) / delta,
|
|
||||||
(probe.totalTorque(1) - before[1]) / delta,
|
|
||||||
(probe.totalTorque(2) - before[2]) / delta);
|
|
||||||
}
|
}
|
||||||
|
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
|
||||||
|
probe.clear();
|
||||||
|
double nudged = input(s, node, state, delta);
|
||||||
|
if (improved) {
|
||||||
|
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
|
||||||
|
} else {
|
||||||
|
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
|
||||||
|
}
|
||||||
|
state.pitchResponseBuilding.add(
|
||||||
|
(probe.totalTorque(0) - before[0]) / delta,
|
||||||
|
(probe.totalTorque(1) - before[1]) / delta,
|
||||||
|
(probe.totalTorque(2) - before[2]) / delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit) {
|
if (commit) {
|
||||||
for (NodeState state : nodeStates.values()) {
|
for (NodeState state : nodeStates.values()) {
|
||||||
state.pitchResponse.set(state.pitchResponseBuilding);
|
state.pitchResponse.set(state.pitchResponseBuilding);
|
||||||
state.yawResponse.set(state.yawResponseBuilding);
|
|
||||||
state.rollResponse.set(state.rollResponseBuilding);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A control surface's input, combined from its two keys and clamped like Warium does. */
|
/** A control surface's input, combined from its two keys and clamped like Warium does. */
|
||||||
private static double input(AeroSurface s, NodeData node, NodeState state, ControlAxis nudged, double extra) {
|
private static double input(AeroSurface s, NodeData node, NodeState state, double extraPitch) {
|
||||||
if (node == null || state == null) {
|
if (node == null || state == null) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
double value = axisValue(s.axis1(), node, state, nudged, extra) * s.multiplier1()
|
double value = axisValue(s.axis1(), node, state, extraPitch) * s.multiplier1()
|
||||||
+ axisValue(s.axis2(), node, state, nudged, extra) * s.multiplier2();
|
+ axisValue(s.axis2(), node, state, extraPitch) * s.multiplier2();
|
||||||
return Math.max(-1.0, Math.min(1.0, value));
|
return Math.max(-1.0, Math.min(1.0, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double axisValue(ControlAxis axis, NodeData node, NodeState state, ControlAxis nudged, double extra) {
|
private static double axisValue(ControlAxis axis, NodeData node, NodeState state, double extraPitch) {
|
||||||
double nudge = axis == nudged ? extra : 0.0;
|
|
||||||
return switch (axis) {
|
return switch (axis) {
|
||||||
case PITCH -> state.pitch + state.trimOutput + nudge;
|
case PITCH -> state.pitch + state.trimOutput + extraPitch;
|
||||||
case YAW -> state.yaw + nudge;
|
case YAW -> state.yaw;
|
||||||
case ROLL -> state.roll + nudge;
|
case ROLL -> state.roll;
|
||||||
case THROTTLE -> node.throttle();
|
case THROTTLE -> node.throttle();
|
||||||
case NONE -> 0.0;
|
case NONE -> 0.0;
|
||||||
};
|
};
|
||||||
@@ -318,12 +293,10 @@ public final class AeroState implements PhysicsBridge.Publishable {
|
|||||||
if (state == null) {
|
if (state == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys
|
state.pitch = moveTowards(state.pitch, node.pitch(), step);
|
||||||
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled);
|
state.yaw = moveTowards(state.yaw, node.yaw(), step);
|
||||||
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
|
state.roll = moveTowards(state.roll, node.roll(), step);
|
||||||
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
|
AutoTrim.update(state, node, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
|
||||||
state.roll = moveTowards(state.roll, effective.roll(), step);
|
|
||||||
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ public record AeroSurface(long key, SurfaceType type, boolean slab, double x, do
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean listensToPitch() {
|
boolean listensToPitch() {
|
||||||
return listensTo(ControlAxis.PITCH);
|
return node != NO_NODE && (axis1 == ControlAxis.PITCH || axis2 == ControlAxis.PITCH);
|
||||||
}
|
|
||||||
|
|
||||||
boolean listensTo(ControlAxis axis) {
|
|
||||||
return node != NO_NODE && (axis1 == axis || axis2 == axis);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
|
||||||
|
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.core.Direction;
|
|
||||||
import net.minecraft.nbt.CompoundTag;
|
|
||||||
import net.minecraft.world.level.Level;
|
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
|
||||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
|
||||||
import net.minecraft.world.level.block.state.properties.Property;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The frame cursor aim steers in, in ship coordinates: forward is the boresight (what Warium's gun reticle points
|
|
||||||
* along, from the HUD block in front of the seat, else the way the seat faces), up is the ship's up axis and right
|
|
||||||
* completes it. Server and client both build it from the same block data, so a cursor sent as components in this
|
|
||||||
* frame means the same on both.
|
|
||||||
*
|
|
||||||
* @param nodePos the Vehicle Control Node the seat is linked to
|
|
||||||
*/
|
|
||||||
public record AimBasis(BlockPos seatPos, BlockPos nodePos, double[] forward, double[] right, double[] up) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param entityPos block position of the seat entity. Warium's control seat block is at that position or just
|
|
||||||
* below it.
|
|
||||||
* @return null if there is no seat block linked to a control node there
|
|
||||||
*/
|
|
||||||
public static AimBasis of(Level level, BlockPos entityPos) {
|
|
||||||
for (BlockPos pos : new BlockPos[]{entityPos, entityPos.below()}) {
|
|
||||||
BlockEntity seat = level.getBlockEntity(pos);
|
|
||||||
if (seat == null || !seat.getPersistentData().contains("ControlX")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CompoundTag tag = seat.getPersistentData();
|
|
||||||
BlockPos nodePos = BlockPos.containing(tag.getDouble("ControlX"), tag.getDouble("ControlY"), tag.getDouble("ControlZ"));
|
|
||||||
return new AimBasis(pos, nodePos, null, null, null).withAxes(level, pos, nodePos);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AimBasis withAxes(Level level, BlockPos seatPos, BlockPos nodePos) {
|
|
||||||
BlockState state = level.getBlockState(seatPos);
|
|
||||||
Direction facing = Direction.NORTH;
|
|
||||||
Property<?> property = state.getBlock().getStateDefinition().getProperty("facing");
|
|
||||||
if (property instanceof DirectionProperty directionProperty) {
|
|
||||||
facing = state.getValue(directionProperty);
|
|
||||||
}
|
|
||||||
double fx = facing.getStepX();
|
|
||||||
double fy = facing.getStepY();
|
|
||||||
double fz = facing.getStepZ();
|
|
||||||
|
|
||||||
// Warium's gun reticle: the HUD block in front of the seat holds the aim vector of its guns
|
|
||||||
BlockEntity hud = level.getBlockEntity(seatPos.relative(facing));
|
|
||||||
if (hud != null && hud.getPersistentData().contains("Pitch")) {
|
|
||||||
CompoundTag hudTag = hud.getPersistentData();
|
|
||||||
double bx = hudTag.getDouble("X") + facing.getStepX();
|
|
||||||
double by = hudTag.getDouble("Pitch");
|
|
||||||
double bz = hudTag.getDouble("Z") + facing.getStepZ();
|
|
||||||
if (bx * bx + by * by + bz * bz > 1.0E-6) {
|
|
||||||
fx = bx;
|
|
||||||
fy = by;
|
|
||||||
fz = bz;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double length = Math.sqrt(fx * fx + fy * fy + fz * fz);
|
|
||||||
if (length < 1.0E-6) {
|
|
||||||
fx = 0.0;
|
|
||||||
fy = 0.0;
|
|
||||||
fz = -1.0;
|
|
||||||
length = 1.0;
|
|
||||||
}
|
|
||||||
fx /= length;
|
|
||||||
fy /= length;
|
|
||||||
fz /= length;
|
|
||||||
|
|
||||||
// right = forward x up(0,1,0)
|
|
||||||
double rx = -fz;
|
|
||||||
double ry = 0.0;
|
|
||||||
double rz = fx;
|
|
||||||
double rl = Math.sqrt(rx * rx + rz * rz);
|
|
||||||
if (rl < 1.0E-6) {
|
|
||||||
rx = 1.0;
|
|
||||||
rz = 0.0;
|
|
||||||
rl = 1.0;
|
|
||||||
}
|
|
||||||
rx /= rl;
|
|
||||||
rz /= rl;
|
|
||||||
// up = right x forward
|
|
||||||
double ux = ry * fz - rz * fy;
|
|
||||||
double uy = rz * fx - rx * fz;
|
|
||||||
double uz = rx * fy - ry * fx;
|
|
||||||
return new AimBasis(seatPos, nodePos, new double[]{fx, fy, fz}, new double[]{rx, ry, rz}, new double[]{ux, uy, uz});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ship-space direction of a cursor given by its components along forward, right and up. */
|
|
||||||
public double[] toShip(double cf, double cr, double cu) {
|
|
||||||
return new double[]{
|
|
||||||
forward[0] * cf + right[0] * cr + up[0] * cu,
|
|
||||||
forward[1] * cf + right[1] * cr + up[1] * cu,
|
|
||||||
forward[2] * cf + right[2] * cr + up[2] * cu};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46,7 +46,7 @@ public final class AutoTrimToggle {
|
|||||||
* The control node the seat under the player is linked to. Warium's control seat block finds its seat entity
|
* The control node the seat under the player is linked to. Warium's control seat block finds its seat entity
|
||||||
* within half a block of itself, so the seat block is at the entity's block or just below it.
|
* within half a block of itself, so the seat block is at the entity's block or just below it.
|
||||||
*/
|
*/
|
||||||
private static BlockEntity findNode(Level level, BlockPos seatPos) {
|
public static BlockEntity findNode(Level level, BlockPos seatPos) {
|
||||||
for (BlockPos pos : new BlockPos[]{seatPos, seatPos.below()}) {
|
for (BlockPos pos : new BlockPos[]{seatPos, seatPos.below()}) {
|
||||||
BlockEntity seat = level.getBlockEntity(pos);
|
BlockEntity seat = level.getBlockEntity(pos);
|
||||||
if (seat == null || !seat.getPersistentData().contains("ControlX")) {
|
if (seat == null || !seat.getPersistentData().contains("ControlX")) {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
|
||||||
|
|
||||||
import dev.lumareth.improvedAerodynamics.network.ControlModePacket;
|
|
||||||
import dev.lumareth.improvedAerodynamics.network.Network;
|
|
||||||
import net.minecraft.nbt.CompoundTag;
|
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
|
||||||
import net.minecraft.world.entity.player.Player;
|
|
||||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
|
||||||
import net.minecraftforge.network.PacketDistributor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How a player flies: with the keyboard (Warium's own seat controls) or with cursor aim. Chosen per player with
|
|
||||||
* {@code /aero controls}, kept in the player's persistent data and told to their client, which does the rest.
|
|
||||||
*/
|
|
||||||
public final class ControlMode {
|
|
||||||
private static final String KEY = "improved_aerodynamics_cursor_aim";
|
|
||||||
|
|
||||||
private ControlMode() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isCursorAim(ServerPlayer player) {
|
|
||||||
CompoundTag persisted = player.getPersistentData().getCompound(Player.PERSISTED_NBT_TAG);
|
|
||||||
return persisted.getBoolean(KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void set(ServerPlayer player, boolean cursorAim) {
|
|
||||||
CompoundTag data = player.getPersistentData();
|
|
||||||
CompoundTag persisted = data.getCompound(Player.PERSISTED_NBT_TAG);
|
|
||||||
persisted.putBoolean(KEY, cursorAim);
|
|
||||||
data.put(Player.PERSISTED_NBT_TAG, persisted);
|
|
||||||
sync(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void sync(ServerPlayer player) {
|
|
||||||
Network.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), new ControlModePacket(isCursorAim(player)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SubscribeEvent
|
|
||||||
public static void onLogin(PlayerEvent.PlayerLoggedInEvent event) {
|
|
||||||
if (event.getEntity() instanceof ServerPlayer player) {
|
|
||||||
sync(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
|
||||||
|
|
||||||
import net.minecraftforge.common.ForgeConfigSpec;
|
|
||||||
|
|
||||||
/** Server side settings of the cursor aim virtual pilot. Lives in improved_aerodynamics-cursor-aim.toml. */
|
|
||||||
public final class CursorAimConfig {
|
|
||||||
public static final ForgeConfigSpec SPEC;
|
|
||||||
|
|
||||||
public static final ForgeConfigSpec.BooleanValue ENABLED;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue FULL_DEFLECTION_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue DEADZONE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_TURN_RATE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_ROLL_RATE_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MAX_BANK_DEGREES;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue BANK_GAIN;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue RATE_GAIN;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue PUSH_FACTOR;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue STALLED_PITCH_FACTOR;
|
|
||||||
public static final ForgeConfigSpec.DoubleValue MIN_SPEED;
|
|
||||||
|
|
||||||
static {
|
|
||||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
|
||||||
builder.comment("Cursor aim: the mouse moves a cursor on screen, a virtual pilot flies the control node's craft by it").push("cursor_aim");
|
|
||||||
ENABLED = builder.comment("Whether pilots may use cursor aim on this server")
|
|
||||||
.define("enabled", true);
|
|
||||||
FULL_DEFLECTION_DEGREES = builder.comment("Angle of the cursor from the gun reticle at which the virtual pilot asks for full deflection: the maximum bank",
|
|
||||||
"sideways, the full pitch rate up or down")
|
|
||||||
.defineInRange("fullDeflectionDegrees", 30.0, 5.0, 90.0);
|
|
||||||
DEADZONE_DEGREES = builder.comment("Radius around the gun reticle in degrees inside which the cursor commands nothing, so the craft flies straight.",
|
|
||||||
"Outside it the command grows from zero, there is no jump at the edge")
|
|
||||||
.defineInRange("deadzoneDegrees", 3.0, 0.0, 15.0);
|
|
||||||
MAX_TURN_RATE_DEGREES = builder.comment("Turn rate in degrees per second the pilot asks for at full deflection, both for the turn at maximum bank and for pitching")
|
|
||||||
.defineInRange("maxTurnRateDegrees", 40.0, 5.0, 360.0);
|
|
||||||
MAX_ROLL_RATE_DEGREES = builder.comment("Fastest roll in degrees per second the pilot asks for when it rolls to a bank angle")
|
|
||||||
.defineInRange("maxRollRateDegrees", 150.0, 10.0, 720.0);
|
|
||||||
MAX_BANK_DEGREES = builder.comment("Bank angle the pilot rolls to when the cursor is at full sideways deflection")
|
|
||||||
.defineInRange("maxBankDegrees", 70.0, 10.0, 90.0);
|
|
||||||
BANK_GAIN = builder.comment("Roll rate in rad/s per radian of bank angle error, how eagerly the pilot rolls to the wanted bank and levels the wings")
|
|
||||||
.defineInRange("bankGain", 3.0, 0.2, 20.0);
|
|
||||||
RATE_GAIN = builder.comment("How hard the pilot corrects its turn rate, in angular acceleration per rad/s of rate error. Lower is smoother,",
|
|
||||||
"higher follows the cursor tighter and can start to oscillate")
|
|
||||||
.defineInRange("rateGain", 5.0, 0.5, 40.0);
|
|
||||||
PUSH_FACTOR = builder.comment("A cursor below the gun reticle pushes the nose down. This is how much of the pull rate that may be, since",
|
|
||||||
"negative g is harder on the craft and the pilot")
|
|
||||||
.defineInRange("pushFactor", 0.4, 0.0, 1.0);
|
|
||||||
STALLED_PITCH_FACTOR = builder.comment("Multiplier for the pitch rate the pilot asks for while the wings are stalled, so it does not keep pulling")
|
|
||||||
.defineInRange("stalledPitchFactor", 0.3, 0.0, 1.0);
|
|
||||||
MIN_SPEED = builder.comment("Below this speed in blocks per second the pilot does nothing, the surfaces have no effect anyway")
|
|
||||||
.defineInRange("minSpeed", 15.0, 0.0, 200.0);
|
|
||||||
builder.pop();
|
|
||||||
SPEC = builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private CursorAimConfig() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Where each control node's pilot currently holds the cursor of cursor aim. Written by the server thread when the
|
|
||||||
* client's packet arrives, read by the physics thread. An entry that is not refreshed for a moment (the player left
|
|
||||||
* the seat, lost the connection) counts as gone.
|
|
||||||
*/
|
|
||||||
public final class CursorAimRegistry {
|
|
||||||
private static final long EXPIRE_NANOS = 500_000_000L;
|
|
||||||
private static final ConcurrentHashMap<Long, Aim> AIMS = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/** Cursor as components along the basis axes (unit length), and the basis itself, in ship space. */
|
|
||||||
public record Aim(double cf, double cr, double cu, double[] forward, double[] right, double[] up, long nanos) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private CursorAimRegistry() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void set(long nodeKey, Aim aim) {
|
|
||||||
AIMS.put(nodeKey, aim);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clear(long nodeKey) {
|
|
||||||
AIMS.remove(nodeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Aim get(long nodeKey) {
|
|
||||||
Aim aim = AIMS.get(nodeKey);
|
|
||||||
if (aim == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (System.nanoTime() - aim.nanos() > EXPIRE_NANOS) {
|
|
||||||
AIMS.remove(nodeKey, aim);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return aim;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isActive(long nodeKey) {
|
|
||||||
return get(nodeKey) != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
|
||||||
|
|
||||||
import net.mcreator.valkyrienwarium.entity.SeatEntityEntity;
|
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
|
|
||||||
/** Server side of cursor aim: takes the cursor a pilot sends and hands it to the control node's virtual pilot. */
|
|
||||||
public final class CursorAimServer {
|
|
||||||
private CursorAimServer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void accept(ServerPlayer player, boolean active, double cf, double cr, double cu) {
|
|
||||||
// Only a player who chose cursor aim may set or clear the cursor of a node
|
|
||||||
if (!ControlMode.isCursorAim(player)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Entity vehicle = player.getVehicle();
|
|
||||||
if (!(vehicle instanceof SeatEntityEntity)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AimBasis basis = AimBasis.of(vehicle.level(), vehicle.blockPosition());
|
|
||||||
if (basis == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
long nodeKey = basis.nodePos().asLong();
|
|
||||||
double length = Math.sqrt(cf * cf + cr * cr + cu * cu);
|
|
||||||
if (!active || !CursorAimConfig.ENABLED.get() || !(length > 1.0E-6) || !Double.isFinite(length)) {
|
|
||||||
CursorAimRegistry.clear(nodeKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
CursorAimRegistry.set(nodeKey, new CursorAimRegistry.Aim(cf / length, cr / length, cu / length,
|
|
||||||
basis.forward(), basis.right(), basis.up(), System.nanoTime()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
|
||||||
|
|
||||||
import org.joml.Matrix3d;
|
|
||||||
import org.joml.Matrix3dc;
|
|
||||||
import org.joml.Quaterniondc;
|
|
||||||
import org.joml.Vector3d;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The virtual pilot of cursor aim. The cursor is a stick that springs back to the gun reticle:
|
|
||||||
* <ul>
|
|
||||||
* <li>sideways it commands a bank angle and turn: the further out, the steeper the bank and the harder the pull
|
|
||||||
* that comes with it. Centered it levels the wings, so a centered cursor is straight flight;</li>
|
|
||||||
* <li>up and down it commands a pitch rate;</li>
|
|
||||||
* <li>close to the reticle it steers with yaw and pitch directly instead of banking, so aiming does not rock the
|
|
||||||
* wings.</li>
|
|
||||||
* </ul>
|
|
||||||
* The wanted rotation is turned into stick inputs by the measured effect of each input on the craft (torque per unit
|
|
||||||
* of input, see {@link AeroState}), so it works for any craft however its surfaces are mixed or oriented: angular
|
|
||||||
* acceleration wanted times inertia, solved for the three inputs with a little regularization for axes that have no
|
|
||||||
* surfaces. Keys the pilot holds win over it, per axis.
|
|
||||||
*/
|
|
||||||
final class CursorPilot {
|
|
||||||
private static final double FINE_START = Math.toRadians(4.0);
|
|
||||||
private static final double FINE_END = Math.toRadians(16.0);
|
|
||||||
private static final double MANUAL_OVERRIDE = 0.05;
|
|
||||||
private static final double MAX_PULL_BANK = Math.toRadians(90.0);
|
|
||||||
|
|
||||||
private CursorPilot() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the node with the pilot's wanted inputs in place of the keys, or the node itself if cursor aim is off
|
|
||||||
*/
|
|
||||||
static NodeData apply(AeroState.NodeState state, NodeData node, Quaterniondc rotation, Matrix3dc inertia, double[] w,
|
|
||||||
double speed, boolean wingStalled) {
|
|
||||||
if (!CursorAimConfig.ENABLED.get()) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
CursorAimRegistry.Aim aim = CursorAimRegistry.get(node.key());
|
|
||||||
if (aim == null || speed < CursorAimConfig.MIN_SPEED.get()) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
double fullAngle = Math.toRadians(CursorAimConfig.FULL_DEFLECTION_DEGREES.get());
|
|
||||||
double maxTurn = Math.toRadians(CursorAimConfig.MAX_TURN_RATE_DEGREES.get());
|
|
||||||
double maxRoll = Math.toRadians(CursorAimConfig.MAX_ROLL_RATE_DEGREES.get());
|
|
||||||
double maxBank = Math.toRadians(CursorAimConfig.MAX_BANK_DEGREES.get());
|
|
||||||
|
|
||||||
double[] f = aim.forward();
|
|
||||||
double[] r = aim.right();
|
|
||||||
double[] u = aim.up();
|
|
||||||
|
|
||||||
// Cursor as angles from the reticle: sideways (positive right) and up, both through any 270 degree cone
|
|
||||||
double sideways = Math.atan2(aim.cr(), aim.cf());
|
|
||||||
double vertical = Math.atan2(aim.cu(), Math.hypot(aim.cf(), aim.cr()));
|
|
||||||
// Deadzone around the reticle: shrink the cursor offset by its radius, direction kept
|
|
||||||
double offset = Math.hypot(sideways, vertical);
|
|
||||||
double deadzone = Math.toRadians(CursorAimConfig.DEADZONE_DEGREES.get());
|
|
||||||
if (offset > 1.0E-9) {
|
|
||||||
double scale = Math.max(0.0, offset - deadzone) / offset;
|
|
||||||
sideways *= scale;
|
|
||||||
vertical *= scale;
|
|
||||||
}
|
|
||||||
double lateral = saturate(sideways / fullAngle);
|
|
||||||
double blend = smoothstep(Math.abs(sideways), FINE_START, FINE_END);
|
|
||||||
|
|
||||||
// Bank of the craft: positive when the right wing is down, through upside down
|
|
||||||
Vector3d rightWorld = rotation.transform(new Vector3d(r[0], r[1], r[2]));
|
|
||||||
Vector3d upWorld = rotation.transform(new Vector3d(u[0], u[1], u[2]));
|
|
||||||
double bank = Math.atan2(-rightWorld.y, upWorld.y);
|
|
||||||
|
|
||||||
double bankCommand = maxBank * lateral * blend;
|
|
||||||
double rollRate = clamp(CursorAimConfig.BANK_GAIN.get() * (bankCommand - bank), -maxRoll, maxRoll);
|
|
||||||
|
|
||||||
// Pitch: the stick, plus the pull that carries the turn while banked
|
|
||||||
double pitchStick = saturate(vertical / fullAngle);
|
|
||||||
double pitchRate = maxTurn * (pitchStick >= 0.0 ? pitchStick : pitchStick * CursorAimConfig.PUSH_FACTOR.get());
|
|
||||||
if (Math.abs(bank) < MAX_PULL_BANK) {
|
|
||||||
pitchRate += maxTurn * Math.abs(lateral) * blend * Math.sin(Math.abs(bank));
|
|
||||||
}
|
|
||||||
if (wingStalled && pitchRate > 0.0) {
|
|
||||||
pitchRate *= CursorAimConfig.STALLED_PITCH_FACTOR.get();
|
|
||||||
}
|
|
||||||
// Close to the reticle the nose follows the cursor with the rudder
|
|
||||||
double yawRate = maxTurn * lateral * (1.0 - blend);
|
|
||||||
|
|
||||||
// Wanted angular velocity in ship axes: roll about forward, pitch about right, yaw (nose right) about -up
|
|
||||||
Vector3d wanted = new Vector3d(
|
|
||||||
rollRate * f[0] + pitchRate * r[0] - yawRate * u[0],
|
|
||||||
rollRate * f[1] + pitchRate * r[1] - yawRate * u[1],
|
|
||||||
rollRate * f[2] + pitchRate * r[2] - yawRate * u[2]);
|
|
||||||
Vector3d error = wanted.sub(w[0], w[1], w[2]);
|
|
||||||
Vector3d acceleration = error.mul(CursorAimConfig.RATE_GAIN.get());
|
|
||||||
Vector3d torque = inertia.transform(acceleration, new Vector3d());
|
|
||||||
|
|
||||||
double[] inputs = solve(state.pitchResponse, state.yawResponse, state.rollResponse, torque);
|
|
||||||
if (inputs == null) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
double pitch = clamp(inputs[0] - state.trimOutput, -1.0, 1.0);
|
|
||||||
double yaw = clamp(inputs[1], -1.0, 1.0);
|
|
||||||
double roll = clamp(inputs[2], -1.0, 1.0);
|
|
||||||
return new NodeData(node.key(),
|
|
||||||
Math.abs(node.pitch()) > MANUAL_OVERRIDE ? node.pitch() : pitch,
|
|
||||||
Math.abs(node.yaw()) > MANUAL_OVERRIDE ? node.yaw() : yaw,
|
|
||||||
Math.abs(node.roll()) > MANUAL_OVERRIDE ? node.roll() : roll,
|
|
||||||
node.throttle(), node.autoTrim(), node.storedTrim());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Least squares for the three inputs whose responses add up to the torque, ridge-regularized so unused axes stay at zero. */
|
|
||||||
private static double[] solve(Vector3d pitch, Vector3d yaw, Vector3d roll, Vector3d torque) {
|
|
||||||
Matrix3d responses = new Matrix3d(pitch, yaw, roll);
|
|
||||||
Matrix3d transposed = responses.transpose(new Matrix3d());
|
|
||||||
Matrix3d normal = transposed.mul(responses, new Matrix3d());
|
|
||||||
double trace = normal.m00() + normal.m11() + normal.m22();
|
|
||||||
if (!(trace > 1.0E-9) || !Double.isFinite(trace)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
double ridge = trace / 3.0 * 1.0E-3;
|
|
||||||
normal.m00(normal.m00() + ridge);
|
|
||||||
normal.m11(normal.m11() + ridge);
|
|
||||||
normal.m22(normal.m22() + ridge);
|
|
||||||
Vector3d right = transposed.transform(torque, new Vector3d());
|
|
||||||
Vector3d x = normal.invert().transform(right);
|
|
||||||
if (!x.isFinite()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new double[]{x.x, x.y, x.z};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double saturate(double value) {
|
|
||||||
return clamp(value, -1.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double smoothstep(double value, double from, double to) {
|
|
||||||
double t = clamp((value - from) / (to - from), 0.0, 1.0);
|
|
||||||
return t * t * (3.0 - 2.0 * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double clamp(double value, double min, double max) {
|
|
||||||
return Math.max(min, Math.min(max, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
public final class MixinPlugin implements IMixinConfigPlugin {
|
public final class MixinPlugin implements IMixinConfigPlugin {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("improved_aerodynamics");
|
private static final Logger LOGGER = LoggerFactory.getLogger("improved_aerodynamics");
|
||||||
private static final String WARIUM_VERSION = "1.1.6";
|
private static final String WARIUM_VERSION = "1.1.7";
|
||||||
private static final String FUTURISM_VERSION = "0.0.4";
|
private static final String FUTURISM_VERSION = "0.0.4";
|
||||||
|
|
||||||
private boolean warium;
|
private boolean warium;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.flight;
|
package dev.lumareth.improvedAerodynamics.flight;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Warium VS 1.1.6's aerodynamic forces, term for term as its block procedures compute them, but evaluated from the
|
* Warium VS 1.1.7's aerodynamic forces, term for term as its block procedures compute them, but evaluated from the
|
||||||
* ship's current motion. Like in Warium every block uses the velocity of the ship's center of mass. The procedures'
|
* ship's current motion. Like in Warium every block uses the velocity of the ship's center of mass. The procedures'
|
||||||
* conditions (air around the block, minimum speed, linked control node) were already checked on the server thread.
|
* conditions (air around the block, minimum speed, linked control node) were already checked on the server thread.
|
||||||
* Quirks are kept on purpose so craft fly exactly as before: vertical wings have no drag (Warium never sets the
|
* Quirks are kept on purpose so craft fly exactly as before: vertical wings have no drag (Warium never sets the
|
||||||
@@ -31,7 +31,8 @@ final class WariumAero {
|
|||||||
switch (s.type()) {
|
switch (s.type()) {
|
||||||
case WING -> {
|
case WING -> {
|
||||||
out.addForce(r, 0.0, v[1] * Math.abs(v[1]) * -45.0 * p / m, 0.0);
|
out.addForce(r, 0.0, v[1] * Math.abs(v[1]) * -45.0 * p / m, 0.0);
|
||||||
out.addForce(r, 0.0, Math.abs(fx * v[0] + fz * v[2]) * 160.0 * p, 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);
|
||||||
double damping = speed * 600.0 * p / m;
|
double damping = speed * 600.0 * p / m;
|
||||||
out.addTorque(-w[0] * damping, -0.1 * w[1] * damping, -w[2] * damping);
|
out.addTorque(-w[0] * damping, -0.1 * w[1] * damping, -w[2] * damping);
|
||||||
double drag = -speed * p / m;
|
double drag = -speed * p / m;
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
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
@@ -0,0 +1,33 @@
|
|||||||
|
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
@@ -0,0 +1,33 @@
|
|||||||
|
package dev.lumareth.improvedAerodynamics.flight.mixin.warium;
|
||||||
|
|
||||||
|
import dev.lumareth.improvedAerodynamics.exhaust.ImprovedExhaust;
|
||||||
|
import net.mcreator.valkyrienwarium.procedures.VTOLExhaustTickProcedure;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.level.LevelAccessor;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
/** The improved exhaust style for the VTOL exhaust: no effect entity, the flame of this mod, see {@link ImprovedExhaust}. */
|
||||||
|
@Mixin(value = VTOLExhaustTickProcedure.class, remap = false)
|
||||||
|
public abstract class VTOLExhaustTickProcedureMixin {
|
||||||
|
@Inject(method = "execute", at = @At("HEAD"))
|
||||||
|
private static void improvedAerodynamics$begin(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
|
||||||
|
ImprovedExhaust.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Redirect(method = "execute", at = @At(value = "INVOKE",
|
||||||
|
target = "Lnet/minecraft/server/level/ServerLevel;m_7967_(Lnet/minecraft/world/entity/Entity;)Z"))
|
||||||
|
private static boolean improvedAerodynamics$addEffect(ServerLevel level, Entity entity) {
|
||||||
|
return ImprovedExhaust.addEffectEntity(level, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject(method = "execute", at = @At("TAIL"))
|
||||||
|
private static void improvedAerodynamics$plume(LevelAccessor world, double x, double y, double z, BlockState blockstate, CallbackInfo ci) {
|
||||||
|
ImprovedExhaust.plume(world, x, y, z, blockstate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.network;
|
|
||||||
|
|
||||||
import dev.lumareth.improvedAerodynamics.flight.CursorAimServer;
|
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
|
||||||
import net.minecraftforge.fml.ModList;
|
|
||||||
import net.minecraftforge.network.NetworkEvent;
|
|
||||||
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sent every tick while a pilot has cursor aim on, and once when it goes off. The cursor is given as components along
|
|
||||||
* the craft's boresight, right and up axes, so it does not depend on the camera.
|
|
||||||
*/
|
|
||||||
public final class AimPacket {
|
|
||||||
private final boolean active;
|
|
||||||
private final float forward;
|
|
||||||
private final float right;
|
|
||||||
private final float up;
|
|
||||||
|
|
||||||
public AimPacket(boolean active, float forward, float right, float up) {
|
|
||||||
this.active = active;
|
|
||||||
this.forward = forward;
|
|
||||||
this.right = right;
|
|
||||||
this.up = up;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void encode(FriendlyByteBuf buffer) {
|
|
||||||
buffer.writeBoolean(active);
|
|
||||||
buffer.writeFloat(forward);
|
|
||||||
buffer.writeFloat(right);
|
|
||||||
buffer.writeFloat(up);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AimPacket decode(FriendlyByteBuf buffer) {
|
|
||||||
return new AimPacket(buffer.readBoolean(), buffer.readFloat(), buffer.readFloat(), buffer.readFloat());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handle(Supplier<NetworkEvent.Context> context) {
|
|
||||||
ServerPlayer player = context.get().getSender();
|
|
||||||
if (player != null && ModList.get().isLoaded("valkyrien_warium")) {
|
|
||||||
CursorAimServer.accept(player, active, forward, right, up);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package dev.lumareth.improvedAerodynamics.network;
|
|
||||||
|
|
||||||
import dev.lumareth.improvedAerodynamics.client.aim.CursorAimClient;
|
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
|
||||||
import net.minecraftforge.fml.DistExecutor;
|
|
||||||
import net.minecraftforge.network.NetworkEvent;
|
|
||||||
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/** Tells a client whether its player flies with cursor aim, see {@code /aero controls}. */
|
|
||||||
public final class ControlModePacket {
|
|
||||||
private final boolean cursorAim;
|
|
||||||
|
|
||||||
public ControlModePacket(boolean cursorAim) {
|
|
||||||
this.cursorAim = cursorAim;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void encode(FriendlyByteBuf buffer) {
|
|
||||||
buffer.writeBoolean(cursorAim);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ControlModePacket decode(FriendlyByteBuf buffer) {
|
|
||||||
return new ControlModePacket(buffer.readBoolean());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handle(Supplier<NetworkEvent.Context> context) {
|
|
||||||
boolean value = cursorAim;
|
|
||||||
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> CursorAimClient.setMode(value));
|
|
||||||
context.get().setPacketHandled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,16 +23,15 @@ public final class Network {
|
|||||||
.decoder(ToggleAutoTrimPacket::decode)
|
.decoder(ToggleAutoTrimPacket::decode)
|
||||||
.consumerMainThread(ToggleAutoTrimPacket::handle)
|
.consumerMainThread(ToggleAutoTrimPacket::handle)
|
||||||
.add();
|
.add();
|
||||||
// High id so it cannot collide with packets added on other branches
|
CHANNEL.messageBuilder(ToggleAfterburnerPacket.class, 1, NetworkDirection.PLAY_TO_SERVER)
|
||||||
CHANNEL.messageBuilder(AimPacket.class, 50, NetworkDirection.PLAY_TO_SERVER)
|
.encoder(ToggleAfterburnerPacket::encode)
|
||||||
.encoder(AimPacket::encode)
|
.decoder(ToggleAfterburnerPacket::decode)
|
||||||
.decoder(AimPacket::decode)
|
.consumerMainThread(ToggleAfterburnerPacket::handle)
|
||||||
.consumerMainThread(AimPacket::handle)
|
|
||||||
.add();
|
.add();
|
||||||
CHANNEL.messageBuilder(ControlModePacket.class, 51, NetworkDirection.PLAY_TO_CLIENT)
|
CHANNEL.messageBuilder(ExhaustPacket.class, 2, NetworkDirection.PLAY_TO_CLIENT)
|
||||||
.encoder(ControlModePacket::encode)
|
.encoder(ExhaustPacket::encode)
|
||||||
.decoder(ControlModePacket::decode)
|
.decoder(ExhaustPacket::decode)
|
||||||
.consumerMainThread(ControlModePacket::handle)
|
.consumerMainThread(ExhaustPacket::handle)
|
||||||
.add();
|
.add();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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,11 +22,13 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional: hands VS empty sections for not yet loaded chunks on a ship's path, so physics sees air there instead of
|
* Optional: hands VS empty sections for not yet loaded chunks on a ship's path, so physics sees air there instead of
|
||||||
* its solid "unloaded" fallback. Only sections fully above the generator's estimated surface (plus a margin) are
|
* its solid "unloaded" fallback. Only sections fully above the generator's estimated surface (plus a margin) are
|
||||||
* replaced, everything lower stays solid so a ship can never end up inside terrain that has not loaded yet.
|
* replaced, everything lower stays solid so a ship can never end up inside terrain that has not loaded yet.
|
||||||
|
* If a height was measured with /aero seedgen, every section from there up is replaced instead.
|
||||||
* Once the real chunk loads, VS overwrites every section of it with the actual terrain.
|
* Once the real chunk loads, VS overwrites every section of it with the actual terrain.
|
||||||
* <p>
|
* <p>
|
||||||
* This uses VS internals (its terrain update API and the mixin field listing its loaded chunks), so it switches
|
* This uses VS internals (its terrain update API and the mixin field listing its loaded chunks), so it switches
|
||||||
@@ -76,6 +78,8 @@ final class AirPlaceholder {
|
|||||||
if (enabled) {
|
if (enabled) {
|
||||||
int budget = Config.MAX_NEW_CHUNKS_PER_TICK.get();
|
int budget = Config.MAX_NEW_CHUNKS_PER_TICK.get();
|
||||||
int surfaceMargin = Config.PLACEHOLDER_SURFACE_MARGIN.get();
|
int surfaceMargin = Config.PLACEHOLDER_SURFACE_MARGIN.get();
|
||||||
|
// Measured with /aero seedgen, replaces the estimate (and its margin) for this dimension
|
||||||
|
OptionalInt airHeight = AirCeiling.get(level).ceiling(level.dimension());
|
||||||
for (long chunk : corridor) {
|
for (long chunk : corridor) {
|
||||||
Placeholder placeholder = placeholders.get(chunk);
|
Placeholder placeholder = placeholders.get(chunk);
|
||||||
if (placeholder != null) {
|
if (placeholder != null) {
|
||||||
@@ -90,7 +94,8 @@ final class AirPlaceholder {
|
|||||||
if (budget-- <= 0) {
|
if (budget-- <= 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
int minSectionY = Math.floorDiv(estimateSurface(level, pos) + surfaceMargin, 16) + 1;
|
int minSectionY = airHeight.isPresent() ? Math.floorDiv(airHeight.getAsInt() + 15, 16)
|
||||||
|
: Math.floorDiv(estimateSurface(level, pos) + surfaceMargin, 16) + 1;
|
||||||
int maxSectionY = level.getMaxSection() - 1;
|
int maxSectionY = level.getMaxSection() - 1;
|
||||||
if (minSectionY > maxSectionY) {
|
if (minSectionY > maxSectionY) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -45,3 +45,11 @@ mandatory = false
|
|||||||
versionRange = "[0.0.4,)"
|
versionRange = "[0.0.4,)"
|
||||||
ordering = "AFTER"
|
ordering = "AFTER"
|
||||||
side = "BOTH"
|
side = "BOTH"
|
||||||
|
|
||||||
|
# Optional, animated block models
|
||||||
|
[[dependencies."${mod_id}"]]
|
||||||
|
modId = "azurelib"
|
||||||
|
mandatory = false
|
||||||
|
versionRange = "[3.2.7,)"
|
||||||
|
ordering = "AFTER"
|
||||||
|
side = "BOTH"
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.8.0",
|
||||||
|
"animations": {
|
||||||
|
"animation.model.nozzle": {
|
||||||
|
"loop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"variants": {
|
||||||
|
"": {
|
||||||
|
"model": "improved_aerodynamics:block/vectored_afterburner_nozzle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"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]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -13,9 +13,14 @@
|
|||||||
"config.improved_aerodynamics.vortexLift": "Wirbelauftrieb: Flügel mit kleiner Streckung (Deltaflügel) überziehen später und liefern bei großen Anstellwinkeln mehr Auftrieb (verbessertes Flugmodell)",
|
"config.improved_aerodynamics.vortexLift": "Wirbelauftrieb: Flügel mit kleiner Streckung (Deltaflügel) überziehen später und liefern bei großen Anstellwinkeln mehr Auftrieb (verbessertes Flugmodell)",
|
||||||
"config.improved_aerodynamics.vortexStallBonus": "Zusätzlicher Überziehwinkel der Flügel mit der kleinsten Streckung, als Anteil des normalen Überziehwinkels",
|
"config.improved_aerodynamics.vortexStallBonus": "Zusätzlicher Überziehwinkel der Flügel mit der kleinsten Streckung, als Anteil des normalen Überziehwinkels",
|
||||||
"config.improved_aerodynamics.vortexLiftStrength": "Multiplikator für den zusätzlichen Auftrieb der Vorderkantenwirbel",
|
"config.improved_aerodynamics.vortexLiftStrength": "Multiplikator für den zusätzlichen Auftrieb der Vorderkantenwirbel",
|
||||||
"key.improved_aerodynamics.cursor_aim_free_look": "Cursor-Steuerung: Freie Sicht (halten)",
|
"block.improved_aerodynamics.vectored_afterburner_nozzle": "Schubvektor-Nachbrennerdüse",
|
||||||
"message.improved_aerodynamics.cursor_aim.on": "Cursor-Steuerung an",
|
"itemGroup.improved_aerodynamics": "Improved Aerodynamics",
|
||||||
"message.improved_aerodynamics.cursor_aim.off": "Cursor-Steuerung aus",
|
"message.improved_aerodynamics.nozzle.left": "Düse auf linkes Triebwerk gestellt",
|
||||||
"message.improved_aerodynamics.cursor_aim.no_seat": "Setz dich in einen Warium-Steuersitz, um die Cursor-Steuerung zu nutzen",
|
"message.improved_aerodynamics.nozzle.right": "Düse auf rechtes Triebwerk gestellt",
|
||||||
"message.improved_aerodynamics.cursor_aim.no_server": "Dieser Server hat Improved Aerodynamics nicht, die Cursor-Steuerung braucht es"
|
"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"
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,14 @@
|
|||||||
"config.improved_aerodynamics.vortexLift": "Vortex lift: low aspect ratio (delta) wings stall later and lift more at high angles of attack (improved flight model)",
|
"config.improved_aerodynamics.vortexLift": "Vortex lift: low aspect ratio (delta) wings stall later and lift more at high angles of attack (improved flight model)",
|
||||||
"config.improved_aerodynamics.vortexStallBonus": "Extra stall angle of the lowest aspect ratio wings, as a fraction of the normal stall angle",
|
"config.improved_aerodynamics.vortexStallBonus": "Extra stall angle of the lowest aspect ratio wings, as a fraction of the normal stall angle",
|
||||||
"config.improved_aerodynamics.vortexLiftStrength": "Multiplier for the extra lift the leading-edge vortices make",
|
"config.improved_aerodynamics.vortexLiftStrength": "Multiplier for the extra lift the leading-edge vortices make",
|
||||||
"key.improved_aerodynamics.cursor_aim_free_look": "Cursor aim: free look (hold)",
|
"block.improved_aerodynamics.vectored_afterburner_nozzle": "Vectored Afterburner Nozzle",
|
||||||
"message.improved_aerodynamics.cursor_aim.on": "Cursor aim on",
|
"itemGroup.improved_aerodynamics": "Improved Aerodynamics",
|
||||||
"message.improved_aerodynamics.cursor_aim.off": "Cursor aim off",
|
"message.improved_aerodynamics.nozzle.left": "Nozzle set to left engine",
|
||||||
"message.improved_aerodynamics.cursor_aim.no_seat": "Sit in a Warium control seat to use cursor aim",
|
"message.improved_aerodynamics.nozzle.right": "Nozzle set to right engine",
|
||||||
"message.improved_aerodynamics.cursor_aim.no_server": "This server does not have Improved Aerodynamics, cursor aim needs it"
|
"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"
|
||||||
}
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"textures": {
|
||||||
|
"particle": "minecraft:block/iron_block"
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"parent": "minecraft:block/cube_all",
|
||||||
|
"textures": {
|
||||||
|
"all": "minecraft:block/iron_block"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"block": "improved_aerodynamics:vectored_afterburner_nozzle",
|
||||||
|
"mass": 500.0,
|
||||||
|
"priority": 101
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"improved_aerodynamics:vectored_afterburner_nozzle"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"improved_aerodynamics:vectored_afterburner_nozzle"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"improved_aerodynamics:vectored_afterburner_nozzle"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"improved_aerodynamics:vectored_afterburner_nozzle"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"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,10 +7,13 @@
|
|||||||
"mixins": [
|
"mixins": [
|
||||||
"futurism.ForceInducedShipsMixin",
|
"futurism.ForceInducedShipsMixin",
|
||||||
"warium.AirBrakeTickProcedureMixin",
|
"warium.AirBrakeTickProcedureMixin",
|
||||||
|
"warium.BypassedCompressorScriptProcedureMixin",
|
||||||
"warium.ControlSurfaceTickProcedureMixin",
|
"warium.ControlSurfaceTickProcedureMixin",
|
||||||
"warium.ForceInducedShipsMixin",
|
"warium.ForceInducedShipsMixin",
|
||||||
"warium.HorizontalStabilizerScriptProcedureMixin",
|
"warium.HorizontalStabilizerScriptProcedureMixin",
|
||||||
"warium.HorizontalWingScriptProcedureMixin",
|
"warium.HorizontalWingScriptProcedureMixin",
|
||||||
|
"warium.JetExhaustOnTickUpdateProcedureMixin",
|
||||||
|
"warium.VTOLExhaustTickProcedureMixin",
|
||||||
"warium.VerticalAirBrakeTickProcedureMixin",
|
"warium.VerticalAirBrakeTickProcedureMixin",
|
||||||
"warium.VerticalControlSurfaceTickProcedureMixin",
|
"warium.VerticalControlSurfaceTickProcedureMixin",
|
||||||
"warium.VerticalWingScriptProcedureMixin"
|
"warium.VerticalWingScriptProcedureMixin"
|
||||||
|
|||||||
Reference in new issue
Block a user