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 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); } } }